GHSA-W4QQ-74H6-58WQ

Vulnerability from github – Published: 2026-05-19 16:25 – Updated: 2026-05-19 16:25
VLAI
Summary
AVideo: Unauthenticated Arbitrary Image Read via Path Traversal in `view/img/image404Raw.php`
Details

Summary

The endpoint requires no authentication. An unauthenticated remote attacker can read arbitrary image files anywhere on disk that the PHP user can open — including private user-profile photos that the application's normal serving wrappers gate behind ACLs, admin-uploaded thumbnails, encrypted-video poster frames, and image content under sibling-app directories reachable via .. traversal.

Details

view/img/image404Raw.php reads the image GET parameter and joins it directly into a filesystem path served via readfile(). view/img/image404Raw.php (full file, current master @ 0dbadbcaaa1b415c7db078a72dc4b26d9fac0485):

<?php

// Fetch requested image URL
$imageURL = !empty($_GET['image']) ? $_GET['image'] : $_SERVER["REQUEST_URI"];
$rootDir = dirname(__FILE__) . '/../../';
if ($imageURL == 'favicon.ico') {
    $imgLocalFile = "{$rootDir}/videos/{$imageURL}";
} else {
    $imgLocalFile = "{$rootDir}/{$imageURL}";   // ← attacker-controlled
}

if (file_exists($imgLocalFile)) {
    $imageInfo = getimagesize($imgLocalFile);   // ← format gate
    if (empty($imageInfo)) {
        die('not image');
    }
    // …extension → Content-Type mapping…
    header("HTTP/1.0 200 OK");
    header('Content-Type: ' . $type);
    header('Content-Length: ' . filesize($imgLocalFile));
    readfile($imgLocalFile);   // ← exfil bytes
    exit;
}

Issues:

  1. No authentication. The file is reachable via direct GET; no require of globals.php, no session check, no API-key gate.
  2. No basename / realpath / prefix containment. $_GET['image'] is concatenated into $imgLocalFile with no .. filtering, no realpath() resolution, no allowlist check against the intended view/img/ directory.
  3. getimagesize() is a magic-bytes check, not a path constraint. Any file on disk whose first bytes match a recognized image format (FFD8FF JPEG, 89504E47 PNG, 474946 GIF, 52494646…57454250 WebP) passes the gate — including images stored outside any ACL'd area of the application.
  4. $_SERVER["REQUEST_URI"] fallback when image is empty widens the attack surface (path components in the URI itself land in $imgLocalFile).

Re-verified pre-submission on 2026-05-13 against view/img/image404Raw.php blob SHA c670b0faff4fbea1fd0508f179956975477d4340 — unsafe shape unchanged since first discovery on 2026-05-12.

Recommended fix — three layered checks, any one alone is insufficient:

// view/img/image404Raw.php — proposed fix
<?php

$imageURL = !empty($_GET['image']) ? $_GET['image'] : '';
if ($imageURL === '') {
    http_response_code(400);
    exit('bad request');
}

// 1. Reject any path-traversal segment outright.
if (strpos($imageURL, '..') !== false
    || strpos($imageURL, "\0") !== false
    || strpos($imageURL, '://') !== false) {
    http_response_code(400);
    exit('bad request');
}

// 2. Resolve to a real path and verify prefix containment under the
//    intended image directory.
$rootDir = realpath(dirname(__FILE__) . '/../../');
$imgLocalFile = realpath($rootDir . '/' . $imageURL);
if ($imgLocalFile === false
    || (strpos($imgLocalFile, $rootDir . '/videos/') !== 0
        && strpos($imgLocalFile, $rootDir . '/view/img/') !== 0)) {
    http_response_code(404);
    exit('not found');
}

// 3. Existing getimagesize() check stays as defense-in-depth.
if (!is_file($imgLocalFile)) {
    http_response_code(404);
    exit('not found');
}
$imageInfo = @getimagesize($imgLocalFile);
if (empty($imageInfo)) {
    http_response_code(404);
    exit('not image');
}

// …rest of the original Content-Type + readfile() flow unchanged…

Drop the $_SERVER["REQUEST_URI"] fallback entirely; if no image parameter is provided, return 400.

PoC

Discovery probe — any HTTP client, no authentication, no cookies:

GET /view/img/image404Raw.php?image=../videos/userPhoto/photo1.jpg HTTP/1.1
Host: avideo.example.com

If videos/userPhoto/photo1.jpg exists on the server, the response is the raw image bytes (HTTP 200, Content-Type: image/jpeg). The application's normal user-photo serving wrapper (which can gate by session / channel ownership) is bypassed entirely.

Cross-directory probe — read images outside the AVideo install root:

GET /view/img/image404Raw.php?image=../../../var/www/other-app/uploads/users/admin.jpg HTTP/1.1
Host: avideo.example.com

If the PHP user has read access to a sibling app's image directory, those files are exfiltrable too.

Enumeration — iterate over predictable numeric IDs:

GET /view/img/image404Raw.php?image=../videos/userPhoto/photo1.jpg
GET /view/img/image404Raw.php?image=../videos/userPhoto/photo2.jpg
GET /view/img/image404Raw.php?image=../videos/userPhoto/photo3.jpg
...

…to harvest all profile images regardless of the application's intended privacy controls.

Impact

Path traversal → arbitrary image read (CWE-22 + CWE-284). Affects any AVideo deployment running master through commit 0dbadbca and likely every release on the supported branches. The attacker:

  1. Bypasses the application's image-content ACLs. Profile photos under videos/userPhoto/ and admin-uploaded private thumbnails that AVideo's normal image-serving wrappers gate by session / channel ownership become readable to any anonymous internet user.
  2. Reads images stored outside the AVideo install root. On shared-hosting / multi-tenant deployments, .. traversal lets the attacker page into sibling-app upload directories — anywhere the PHP user has read access on disk and the target file's first bytes form a valid image header.
  3. Enables enumeration at scale. Numeric ID schemes (photo1.jpg, photo2.jpg, …) and predictable filenames let an attacker harvest every private image on a deployment without detection (each request looks like a single 200-image-OK to the web log).

Because the read primitive is restricted to image-magic-bytes files, there is no source-code or credential exfiltration via this primitive alone — but the privacy / GDPR exposure is substantial on any deployment that hosts user-uploaded photos. CVSS 5.3 (Medium) reflects the limited but real confidentiality impact; many operators will rate this higher because the leaked content is user-private by intent.

This is not a silent-fix disclosure — the bug is still present on current master at submission time; the maintainer is being notified of a previously-unknown issue.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "Packagist",
        "name": "WWBN/AVideo"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "last_affected": "29.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-46337"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-22",
      "CWE-284",
      "CWE-862"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-05-19T16:25:27Z",
    "nvd_published_at": null,
    "severity": "MODERATE"
  },
  "details": "### Summary\nThe endpoint requires **no authentication**. An unauthenticated remote attacker can read arbitrary image files anywhere on disk that the PHP user can open \u2014 including private user-profile photos that the application\u0027s normal serving wrappers gate behind ACLs, admin-uploaded thumbnails, encrypted-video poster frames, and image content under sibling-app directories reachable via `..` traversal.\n\n### Details\n`view/img/image404Raw.php` reads the `image` GET parameter and joins it directly into a filesystem path served via `readfile()`.  `view/img/image404Raw.php` (full file, current `master` @ `0dbadbcaaa1b415c7db078a72dc4b26d9fac0485`):\n\n```php\n\u003c?php\n\n// Fetch requested image URL\n$imageURL = !empty($_GET[\u0027image\u0027]) ? $_GET[\u0027image\u0027] : $_SERVER[\"REQUEST_URI\"];\n$rootDir = dirname(__FILE__) . \u0027/../../\u0027;\nif ($imageURL == \u0027favicon.ico\u0027) {\n    $imgLocalFile = \"{$rootDir}/videos/{$imageURL}\";\n} else {\n    $imgLocalFile = \"{$rootDir}/{$imageURL}\";   // \u2190 attacker-controlled\n}\n\nif (file_exists($imgLocalFile)) {\n    $imageInfo = getimagesize($imgLocalFile);   // \u2190 format gate\n    if (empty($imageInfo)) {\n        die(\u0027not image\u0027);\n    }\n    // \u2026extension \u2192 Content-Type mapping\u2026\n    header(\"HTTP/1.0 200 OK\");\n    header(\u0027Content-Type: \u0027 . $type);\n    header(\u0027Content-Length: \u0027 . filesize($imgLocalFile));\n    readfile($imgLocalFile);   // \u2190 exfil bytes\n    exit;\n}\n```\n\nIssues:\n\n1. **No authentication.** The file is reachable via direct GET; no `require` of `globals.php`, no session check, no API-key gate.\n2. **No basename / realpath / prefix containment.** `$_GET[\u0027image\u0027]`  is concatenated into `$imgLocalFile` with no `..` filtering, no `realpath()` resolution, no allowlist check against the intended `view/img/` directory.\n3. **`getimagesize()` is a magic-bytes check, not a path constraint.**  Any file on disk whose first bytes match a recognized image format (`FFD8FF` JPEG, `89504E47` PNG, `474946` GIF, `52494646\u202657454250` WebP) passes the gate \u2014 including images stored outside any ACL\u0027d area of the application.\n4. **`$_SERVER[\"REQUEST_URI\"]` fallback** when `image` is empty widens the attack surface (path components in the URI itself land in `$imgLocalFile`).\n\n**Re-verified pre-submission** on 2026-05-13 against `view/img/image404Raw.php` blob SHA `c670b0faff4fbea1fd0508f179956975477d4340` \u2014 unsafe shape unchanged since first discovery on 2026-05-12.\n\n**Recommended fix** \u2014 three layered checks, any one alone is insufficient:\n\n```php\n// view/img/image404Raw.php \u2014 proposed fix\n\u003c?php\n\n$imageURL = !empty($_GET[\u0027image\u0027]) ? $_GET[\u0027image\u0027] : \u0027\u0027;\nif ($imageURL === \u0027\u0027) {\n    http_response_code(400);\n    exit(\u0027bad request\u0027);\n}\n\n// 1. Reject any path-traversal segment outright.\nif (strpos($imageURL, \u0027..\u0027) !== false\n    || strpos($imageURL, \"\\0\") !== false\n    || strpos($imageURL, \u0027://\u0027) !== false) {\n    http_response_code(400);\n    exit(\u0027bad request\u0027);\n}\n\n// 2. Resolve to a real path and verify prefix containment under the\n//    intended image directory.\n$rootDir = realpath(dirname(__FILE__) . \u0027/../../\u0027);\n$imgLocalFile = realpath($rootDir . \u0027/\u0027 . $imageURL);\nif ($imgLocalFile === false\n    || (strpos($imgLocalFile, $rootDir . \u0027/videos/\u0027) !== 0\n        \u0026\u0026 strpos($imgLocalFile, $rootDir . \u0027/view/img/\u0027) !== 0)) {\n    http_response_code(404);\n    exit(\u0027not found\u0027);\n}\n\n// 3. Existing getimagesize() check stays as defense-in-depth.\nif (!is_file($imgLocalFile)) {\n    http_response_code(404);\n    exit(\u0027not found\u0027);\n}\n$imageInfo = @getimagesize($imgLocalFile);\nif (empty($imageInfo)) {\n    http_response_code(404);\n    exit(\u0027not image\u0027);\n}\n\n// \u2026rest of the original Content-Type + readfile() flow unchanged\u2026\n```\n\nDrop the `$_SERVER[\"REQUEST_URI\"]` fallback entirely; if no `image`\nparameter is provided, return 400.\n\n### PoC\n\nDiscovery probe \u2014 any HTTP client, no authentication, no cookies:\n\n```http\nGET /view/img/image404Raw.php?image=../videos/userPhoto/photo1.jpg HTTP/1.1\nHost: avideo.example.com\n```\n\nIf `videos/userPhoto/photo1.jpg` exists on the server, the response is the raw image bytes (HTTP 200, `Content-Type: image/jpeg`). The application\u0027s normal user-photo serving wrapper (which can gate by session / channel ownership) is bypassed entirely.\n\nCross-directory probe \u2014 read images outside the AVideo install root:\n\n```http\nGET /view/img/image404Raw.php?image=../../../var/www/other-app/uploads/users/admin.jpg HTTP/1.1\nHost: avideo.example.com\n```\n\nIf the PHP user has read access to a sibling app\u0027s image directory, those files are exfiltrable too.\n\nEnumeration \u2014 iterate over predictable numeric IDs:\n\n```\nGET /view/img/image404Raw.php?image=../videos/userPhoto/photo1.jpg\nGET /view/img/image404Raw.php?image=../videos/userPhoto/photo2.jpg\nGET /view/img/image404Raw.php?image=../videos/userPhoto/photo3.jpg\n...\n```\n\n\u2026to harvest all profile images regardless of the application\u0027s intended privacy controls.\n\n### Impact\n\n**Path traversal \u2192 arbitrary image read (CWE-22 + CWE-284).** Affects any AVideo deployment running master through commit `0dbadbca` and likely every release on the supported branches. The attacker:\n\n1. **Bypasses the application\u0027s image-content ACLs.** Profile photos under `videos/userPhoto/` and admin-uploaded private thumbnails  that AVideo\u0027s normal image-serving wrappers gate by session / channel ownership become readable to any anonymous internet user.\n2. **Reads images stored outside the AVideo install root.** On shared-hosting / multi-tenant deployments, `..` traversal lets the  attacker page into sibling-app upload directories \u2014 anywhere the PHP user has read access on disk and the target file\u0027s first bytes form a valid image header.\n3. **Enables enumeration at scale.** Numeric ID schemes (`photo1.jpg`, `photo2.jpg`, \u2026) and predictable filenames let an attacker harvest every private image on a deployment without detection (each request looks like a single 200-image-OK to the web log).\n\nBecause the read primitive is restricted to image-magic-bytes files, there is no source-code or credential exfiltration via this primitive alone \u2014 but the **privacy / GDPR exposure** is substantial on any deployment that hosts user-uploaded photos. CVSS 5.3 (Medium) reflects the limited but real confidentiality impact; many operators will rate this higher because the leaked content is user-private by intent.\n\nThis is **not** a silent-fix disclosure \u2014 the bug is still present on current `master` at submission time; the maintainer is being\nnotified of a previously-unknown issue.",
  "id": "GHSA-w4qq-74h6-58wq",
  "modified": "2026-05-19T16:25:27Z",
  "published": "2026-05-19T16:25:27Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/WWBN/AVideo/security/advisories/GHSA-w4qq-74h6-58wq"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/WWBN/AVideo"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N",
      "type": "CVSS_V4"
    }
  ],
  "summary": "AVideo: Unauthenticated Arbitrary Image Read via Path Traversal in `view/img/image404Raw.php`"
}


Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

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…