GHSA-JW8G-5J46-44RP

Vulnerability from github – Published: 2026-05-05 19:13 – Updated: 2026-05-13 14:19
VLAI
Summary
AVideo: CSRF in userSavePhoto.php Allows Cross-Origin Overwrite of Authenticated Users' Profile Photos with Arbitrary Content
Details

Summary

objects/userSavePhoto.php is a legacy profile-photo endpoint that accepts a base64 POST parameter and writes the decoded bytes to videos/userPhoto/photo<users_id>.png. Its only access control is User::isLogged(). It does not end in .json.php, so it is excluded from the project's global autoCSRFGuard (which is suffix-scoped in objects/include_config.php). There is no CSRF token, no Origin/Referer check, and no MIME validation of the decoded bytes. Because AVideo's default cookie policy is SameSite=None; Secure on HTTPS (objects/functionsPHP.php:227), an attacker who lures a logged-in user to a malicious page can overwrite that user's profile photo with arbitrary bytes and also triggers a site-wide clearCache(true) on every forged request.

Details

Handler (objects/userSavePhoto.php, 51 lines total):

// line 12 - only access control
if (!User::isLogged()) {
    $obj->msg = __("You must be logged");
    die(json_encode($obj));
}
// ...
// line 29 - unvalidated base64 from POST
$fileData = base64DataToImage($_POST['imgBase64']);
// line 30 - deterministic filename tied to the VICTIM's session
$fileName = 'photo'. User::getId().'.png';
$photoURL = $imagePath.$fileName;
// line 35 - raw bytes written to disk
$bytes = file_put_contents($global['systemRootPath'].$photoURL, $fileData);
// lines 43-48 - DB update + global cache invalidation unconditionally
$user = new User(User::getId());
$user->setPhotoURL($photoURL);
if ($user->save()) {
    User::deleteOGImage(User::getId());
    User::updateSessionInfo();
    clearCache(true);
}

base64DataToImage (objects/functionsImages.php:1026) performs no content validation:

function base64DataToImage($imgBase64) {
    $img = $imgBase64;
    $img = str_replace('data:image/png;base64,', '', $img);
    $img = str_replace(' ', '+', $img);
    return base64_decode($img);
}

There is no call to getimagesizefromstring, imagecreatefromstring, or MIME detection. Arbitrary bytes up to post_max_size are accepted.

Why the global CSRF guard does not apply. objects/include_config.php (around line 314) only invokes autoCSRFGuard when the script filename matches *.json.php:

if (... $_SERVER['REQUEST_METHOD'] === 'POST' &&
    substr($baseName, -9) === '.json.php') {
    autoCSRFGuard($baseName, $_SERVER['SCRIPT_FILENAME']);
}

userSavePhoto.php is missing the .json.php suffix, so neither autoCSRFGuard nor forbidIfIsUntrustedRequest runs. There is no explicit call to any of these in the file (verified by grep: no getCSRF, no forbidIfIsUntrustedRequest, no HTTP_ORIGIN, no HTTP_REFERER). Routing rewrites in .htaccess also expose this handler as /savePhoto.

Why the victim's cookie is sent cross-origin. objects/functionsPHP.php:227:

function _getCookieSameSiteValue($secure) {
    return $secure ? 'None' : 'Lax';
}

On HTTPS (the expected deployment), session cookies default to SameSite=None; Secure, which browsers attach to cross-site POSTs. A plain application/x-www-form-urlencoded form POST is a "simple request" under CORS rules and does not trigger a preflight, so the browser sends the POST and its cookie without the server having to opt in.

PoC

  1. Victim logs into the AVideo instance (e.g., https://victim.example.com). PHPSESSID is set with SameSite=None; Secure.
  2. Attacker hosts the following HTML on any domain:
<!doctype html>
<html><body>
<form id="f" action="https://victim.example.com/objects/userSavePhoto.php" method="POST">
  <!-- Any bytes: here, 'HELLO WORLD' base64-encoded -->
  <input name="imgBase64" value="SEVMTE8gV09STEQ=">
</form>
<script>document.forms[0].submit();</script>
</body></html>
  1. Victim visits the attacker page in the same browser. The form auto-submits. The browser sends the POST with the victim's session cookie.
  2. userSavePhoto.php passes the User::isLogged() check, decodes the base64, and writes the raw bytes to videos/userPhoto/photo<VICTIM_USERS_ID>.png. It also calls $user->save(), User::deleteOGImage(), User::updateSessionInfo(), and clearCache(true).
  3. Fetching https://victim.example.com/videos/userPhoto/photo<VICTIM_USERS_ID>.png (the file is now the attacker's bytes — HELLO WORLD in this test case). The response is 200 OK and the body equals the submitted bytes.

Replace the imgBase64 payload with a valid PNG to make the defacement visually persuasive, or with up to ~6 MB of any bytes to force a large write.

Impact

  • Integrity — profile defacement of any logged-in user. One click lets an attacker replace a victim's profile photo with arbitrary bytes: offensive imagery, misleading branding, or a clone of another user's photo for impersonation. The file path is deterministic (photo<users_id>.png), so the attacker can later direct others to the overwritten URL.
  • Availability — global cache thrash. Every successful forged request calls clearCache(true), invalidating application-wide caches. Repeatedly tricking logged-in users into visiting the attacker page (e.g., by including the payload as a hidden iframe on a popular site) produces sustained cache invalidation.
  • Availability — disk pressure. With no size cap beyond PHP's post_max_size (default 8 MB → ~6 MB after base64 decode), each forged submission writes a multi-megabyte file. Across many victims this enables distributed disk exhaustion.
  • No confidentiality impact and no code execution (files are served with Content-Type: image/png based on extension, so SVG-with-script payloads are not interpreted).
  • Related endpoints. objects/userSaveBackground.php exhibits the same pattern (same base64DataToImage sink, same lack of CSRF/Origin/MIME checks) and is exploitable identically; fix should be applied consistently.

Recommended Fix

Apply the existing same-origin guard that protects the *.json.php endpoints and add content validation. In objects/userSavePhoto.php, immediately after the login check:

require_once $global['systemRootPath'] . 'objects/functionsSecurity.php';
forbidIfIsUntrustedRequest('userSavePhoto');

$raw = $_POST['imgBase64'] ?? '';
if (strlen($raw) > 2 * 1024 * 1024) { // ~1.5 MB decoded cap
    $obj->msg = __('Image too large');
    die(json_encode($obj));
}
$fileData = base64DataToImage($raw);
if ($fileData === false || $fileData === '' || @imagecreatefromstring($fileData) === false) {
    $obj->msg = __('Invalid image');
    die(json_encode($obj));
}

The longer-term fix is to broaden the global guard in objects/include_config.php so that autoCSRFGuard covers every authenticated POST handler, not only those whose filenames end in .json.php — the current suffix-based gating is a footgun that silently excludes legacy endpoints like userSavePhoto.php and userSaveBackground.php. Also consider moving the clearCache(true) call inside the if ($bytes) branch so that zero-byte writes do not invalidate the global cache.

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-43877"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-352"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-05-05T19:13:03Z",
    "nvd_published_at": "2026-05-11T22:22:12Z",
    "severity": "MODERATE"
  },
  "details": "## Summary\n\n`objects/userSavePhoto.php` is a legacy profile-photo endpoint that accepts a base64 POST parameter and writes the decoded bytes to `videos/userPhoto/photo\u003cusers_id\u003e.png`. Its only access control is `User::isLogged()`. It does not end in `.json.php`, so it is excluded from the project\u0027s global `autoCSRFGuard` (which is suffix-scoped in `objects/include_config.php`). There is no CSRF token, no Origin/Referer check, and no MIME validation of the decoded bytes. Because AVideo\u0027s default cookie policy is `SameSite=None; Secure` on HTTPS (`objects/functionsPHP.php:227`), an attacker who lures a logged-in user to a malicious page can overwrite that user\u0027s profile photo with arbitrary bytes and also triggers a site-wide `clearCache(true)` on every forged request.\n\n## Details\n\nHandler (`objects/userSavePhoto.php`, 51 lines total):\n\n```php\n// line 12 - only access control\nif (!User::isLogged()) {\n    $obj-\u003emsg = __(\"You must be logged\");\n    die(json_encode($obj));\n}\n// ...\n// line 29 - unvalidated base64 from POST\n$fileData = base64DataToImage($_POST[\u0027imgBase64\u0027]);\n// line 30 - deterministic filename tied to the VICTIM\u0027s session\n$fileName = \u0027photo\u0027. User::getId().\u0027.png\u0027;\n$photoURL = $imagePath.$fileName;\n// line 35 - raw bytes written to disk\n$bytes = file_put_contents($global[\u0027systemRootPath\u0027].$photoURL, $fileData);\n// lines 43-48 - DB update + global cache invalidation unconditionally\n$user = new User(User::getId());\n$user-\u003esetPhotoURL($photoURL);\nif ($user-\u003esave()) {\n    User::deleteOGImage(User::getId());\n    User::updateSessionInfo();\n    clearCache(true);\n}\n```\n\n`base64DataToImage` (`objects/functionsImages.php:1026`) performs no content validation:\n\n```php\nfunction base64DataToImage($imgBase64) {\n    $img = $imgBase64;\n    $img = str_replace(\u0027data:image/png;base64,\u0027, \u0027\u0027, $img);\n    $img = str_replace(\u0027 \u0027, \u0027+\u0027, $img);\n    return base64_decode($img);\n}\n```\n\nThere is no call to `getimagesizefromstring`, `imagecreatefromstring`, or MIME detection. Arbitrary bytes up to `post_max_size` are accepted.\n\n**Why the global CSRF guard does not apply.** `objects/include_config.php` (around line 314) only invokes `autoCSRFGuard` when the script filename matches `*.json.php`:\n\n```php\nif (... $_SERVER[\u0027REQUEST_METHOD\u0027] === \u0027POST\u0027 \u0026\u0026\n    substr($baseName, -9) === \u0027.json.php\u0027) {\n    autoCSRFGuard($baseName, $_SERVER[\u0027SCRIPT_FILENAME\u0027]);\n}\n```\n\n`userSavePhoto.php` is missing the `.json.php` suffix, so neither `autoCSRFGuard` nor `forbidIfIsUntrustedRequest` runs. There is no explicit call to any of these in the file (verified by grep: no `getCSRF`, no `forbidIfIsUntrustedRequest`, no `HTTP_ORIGIN`, no `HTTP_REFERER`). Routing rewrites in `.htaccess` also expose this handler as `/savePhoto`.\n\n**Why the victim\u0027s cookie is sent cross-origin.** `objects/functionsPHP.php:227`:\n\n```php\nfunction _getCookieSameSiteValue($secure) {\n    return $secure ? \u0027None\u0027 : \u0027Lax\u0027;\n}\n```\n\nOn HTTPS (the expected deployment), session cookies default to `SameSite=None; Secure`, which browsers attach to cross-site POSTs. A plain `application/x-www-form-urlencoded` form POST is a \"simple request\" under CORS rules and does not trigger a preflight, so the browser sends the POST and its cookie without the server having to opt in.\n\n## PoC\n\n1. Victim logs into the AVideo instance (e.g., `https://victim.example.com`). `PHPSESSID` is set with `SameSite=None; Secure`.\n2. Attacker hosts the following HTML on any domain:\n\n```html\n\u003c!doctype html\u003e\n\u003chtml\u003e\u003cbody\u003e\n\u003cform id=\"f\" action=\"https://victim.example.com/objects/userSavePhoto.php\" method=\"POST\"\u003e\n  \u003c!-- Any bytes: here, \u0027HELLO WORLD\u0027 base64-encoded --\u003e\n  \u003cinput name=\"imgBase64\" value=\"SEVMTE8gV09STEQ=\"\u003e\n\u003c/form\u003e\n\u003cscript\u003edocument.forms[0].submit();\u003c/script\u003e\n\u003c/body\u003e\u003c/html\u003e\n```\n\n3. Victim visits the attacker page in the same browser. The form auto-submits. The browser sends the POST with the victim\u0027s session cookie.\n4. `userSavePhoto.php` passes the `User::isLogged()` check, decodes the base64, and writes the raw bytes to `videos/userPhoto/photo\u003cVICTIM_USERS_ID\u003e.png`. It also calls `$user-\u003esave()`, `User::deleteOGImage()`, `User::updateSessionInfo()`, and `clearCache(true)`.\n5. Fetching `https://victim.example.com/videos/userPhoto/photo\u003cVICTIM_USERS_ID\u003e.png` (the file is now the attacker\u0027s bytes \u2014 `HELLO WORLD` in this test case). The response is `200 OK` and the body equals the submitted bytes.\n\nReplace the `imgBase64` payload with a valid PNG to make the defacement visually persuasive, or with up to ~6 MB of any bytes to force a large write.\n\n## Impact\n\n- **Integrity \u2014 profile defacement of any logged-in user.** One click lets an attacker replace a victim\u0027s profile photo with arbitrary bytes: offensive imagery, misleading branding, or a clone of another user\u0027s photo for impersonation. The file path is deterministic (`photo\u003cusers_id\u003e.png`), so the attacker can later direct others to the overwritten URL.\n- **Availability \u2014 global cache thrash.** Every successful forged request calls `clearCache(true)`, invalidating application-wide caches. Repeatedly tricking logged-in users into visiting the attacker page (e.g., by including the payload as a hidden iframe on a popular site) produces sustained cache invalidation.\n- **Availability \u2014 disk pressure.** With no size cap beyond PHP\u0027s `post_max_size` (default 8 MB \u2192 ~6 MB after base64 decode), each forged submission writes a multi-megabyte file. Across many victims this enables distributed disk exhaustion.\n- **No confidentiality impact** and no code execution (files are served with `Content-Type: image/png` based on extension, so SVG-with-script payloads are not interpreted).\n- **Related endpoints.** `objects/userSaveBackground.php` exhibits the same pattern (same `base64DataToImage` sink, same lack of CSRF/Origin/MIME checks) and is exploitable identically; fix should be applied consistently.\n\n## Recommended Fix\n\nApply the existing same-origin guard that protects the `*.json.php` endpoints and add content validation. In `objects/userSavePhoto.php`, immediately after the login check:\n\n```php\nrequire_once $global[\u0027systemRootPath\u0027] . \u0027objects/functionsSecurity.php\u0027;\nforbidIfIsUntrustedRequest(\u0027userSavePhoto\u0027);\n\n$raw = $_POST[\u0027imgBase64\u0027] ?? \u0027\u0027;\nif (strlen($raw) \u003e 2 * 1024 * 1024) { // ~1.5 MB decoded cap\n    $obj-\u003emsg = __(\u0027Image too large\u0027);\n    die(json_encode($obj));\n}\n$fileData = base64DataToImage($raw);\nif ($fileData === false || $fileData === \u0027\u0027 || @imagecreatefromstring($fileData) === false) {\n    $obj-\u003emsg = __(\u0027Invalid image\u0027);\n    die(json_encode($obj));\n}\n```\n\nThe longer-term fix is to broaden the global guard in `objects/include_config.php` so that `autoCSRFGuard` covers every authenticated POST handler, not only those whose filenames end in `.json.php` \u2014 the current suffix-based gating is a footgun that silently excludes legacy endpoints like `userSavePhoto.php` and `userSaveBackground.php`. Also consider moving the `clearCache(true)` call inside the `if ($bytes)` branch so that zero-byte writes do not invalidate the global cache.",
  "id": "GHSA-jw8g-5j46-44rp",
  "modified": "2026-05-13T14:19:40Z",
  "published": "2026-05-05T19:13:03Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/WWBN/AVideo/security/advisories/GHSA-jw8g-5j46-44rp"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-43877"
    },
    {
      "type": "WEB",
      "url": "https://github.com/WWBN/AVideo/commit/9c38468041505e637101c5943c5370c68f48e3ac"
    },
    {
      "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:R/S:U/C:N/I:L/A:L",
      "type": "CVSS_V3"
    }
  ],
  "summary": "AVideo: CSRF in userSavePhoto.php Allows Cross-Origin Overwrite of Authenticated Users\u0027 Profile Photos with Arbitrary Content"
}


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…