{"id":"CVE-2026-43877","aliases":["GHSA-jw8g-5j46-44rp"],"url":"https://o3.security/vulnerability/CVE-2026-43877","summary":"WWBN AVideo: CSRF in userSavePhoto.php Allows Cross-Origin Overwrite of Any Logged-in User's Profile Photo with Arbitrary Bytes","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<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.\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->msg = __(\"You must be logged\");\n    die(json_encode($obj));\n}\n// ...\n// line 29 - unvalidated base64 from POST\n$fileData = base64DataToImage($_POST['imgBase64']);\n// line 30 - deterministic filename tied to the VICTIM's session\n$fileName = 'photo'. User::getId().'.png';\n$photoURL = $imagePath.$fileName;\n// line 35 - raw bytes written to disk\n$bytes = file_put_contents($global['systemRootPath'].$photoURL, $fileData);\n// lines 43-48 - DB update + global cache invalidation unconditionally\n$user = new User(User::getId());\n$user->setPhotoURL($photoURL);\nif ($user->save()) {\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('data:image/png;base64,', '', $img);\n    $img = str_replace(' ', '+', $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['REQUEST_METHOD'] === 'POST' &&\n    substr($baseName, -9) === '.json.php') {\n    autoCSRFGuard($baseName, $_SERVER['SCRIPT_FILENAME']);\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's cookie is sent cross-origin.** `objects/functionsPHP.php:227`:\n\n```php\nfunction _getCookieSameSiteValue($secure) {\n    return $secure ? 'None' : 'Lax';\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<!doctype html>\n<html><body>\n<form id=\"f\" action=\"https://victim.example.com/objects/userSavePhoto.php\" method=\"POST\">\n  <!-- Any bytes: here, 'HELLO WORLD' base64-encoded -->\n  <input name=\"imgBase64\" value=\"SEVMTE8gV09STEQ=\">\n</form>\n<script>document.forms[0].submit();</script>\n</body></html>\n```\n\n3. Victim visits the attacker page in the same browser. The form auto-submits. The browser sends the POST with the victim's session cookie.\n4. `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)`.\n5. 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.\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 — 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.\n- **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.\n- **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.\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['systemRootPath'] . 'objects/functionsSecurity.php';\nforbidIfIsUntrustedRequest('userSavePhoto');\n\n$raw = $_POST['imgBase64'] ?? '';\nif (strlen($raw) > 2 * 1024 * 1024) { // ~1.5 MB decoded cap\n    $obj->msg = __('Image too large');\n    die(json_encode($obj));\n}\n$fileData = base64DataToImage($raw);\nif ($fileData === false || $fileData === '' || @imagecreatefromstring($fileData) === false) {\n    $obj->msg = __('Invalid image');\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` — 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.","published":"2026-05-11T20:34:43.371Z","modified":"2026-08-12T03:51:37.443235293Z","cvss":{"score":5.4,"severity":"MEDIUM","vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:L/A:L"},"epss":{"score":0.00121,"percentile":0.02111,"asOf":"2026-08-24"},"cisaKev":null,"exploitsKnown":0,"affectedPackages":[{"ecosystem":"Packagist","name":"wwbn/avideo","fixedVersion":null}],"fix":{"url":"https://github.com/WWBN/AVideo/commit/9c38468041505e637101c5943c5370c68f48e3ac","label":"WWBN/AVideo@9c38468"},"references":[{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/43xxx/CVE-2026-43877.json"},{"type":"ADVISORY","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":"FIX","url":"https://github.com/WWBN/AVideo/commit/9c38468041505e637101c5943c5370c68f48e3ac"},{"type":"PACKAGE","url":"https://github.com/WWBN/AVideo"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-08-12T03:51:37.443235293Z"}}