GitHub Security Advisory: GHSA-8wf4-c4x3-h952
Package: wwbn/avideo (COMPOSER)
Severity: HIGH (CVSS 8.8)
CWE: CWE-434: Unrestricted Upload of File with Dangerous Type
Vulnerable versions: <= 26.0
Patched in: unpatched

Summary: AVideo: Remote Code Execution via PHP Temp File in Encoder downloadURL

Details:
## Summary

The `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/`.

## Details

The vulnerability is a race-free file upload leading to RCE, exploiting a logic flaw in the error handling order of operations.

**Step 1 — File download preserves dangerous extension:**

In `objects/aVideoEncoder.json.php`, when a `downloadURL` parameter is provided, the file is downloaded and saved with the URL's original basename:

```php
// objects/aVideoEncoder.json.php:361-365
$_FILES['video']['name'] = basename($downloadURL);  // preserves .php extension
$temp = Video::getStoragePath() . "cache/tmpFile/" . $_FILES['video']['name'];
make_path($temp);
$bytesSaved = file_put_contents($temp, $file);
```

The `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`.

**Step 2 — Resolution validation aborts after file write:**

After the file is downloaded and written to disk (line 156), the resolution is validated:

```php
// objects/aVideoEncoder.json.php:229-233
if (!in_array($_REQUEST['resolution'], $global['avideo_possible_resolutions'])) {
    $msg = "This resolution is not possible {$_REQUEST['resolution']}";
    _error_log($msg);
    forbiddenPage($msg);  // calls die() — execution stops here
}
```

The `forbiddenPage()` function (in `objects/functionsSecurity.php:567-573`) detects the JSON content type set at line 26 and calls `die()`:

```php
if (empty($unlockPassword) && isContentTypeJson()) {
    // ...
    die(json_encode($obj));  // line 573 — execution terminates
}
```

**Step 3 — Cleanup never reached:**

The `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.

**Step 4 — No execution restrictions on temp directory:**

The `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`.

## PoC

**Prerequisites:** An authenticated user account with `canUpload` permission. An attacker-controlled server hosting a PHP payload file at least 20KB in size.

**Step 1 — Prepare the PHP payload (on attacker server):**

```bash
# Create a PHP webshell padded to >=20KB to pass the minimum size check
python3 -c "
payload = b'<?php