GHSA-q4ph-8x8g-95f8 AzuraCast Vulnerable to Liquidsoap Code Injection via Incomplete cleanUpString-to-toRawString Migration in Remote Relay Password Field
SummaryThe `cleanUpString()` method in `ConfigWriter.php` uses an ungreedy regex to strip Liquidsoap string interpolation patterns (`#{...}`) from user input. This regex can be bypassed via nested interpolation syntax (`#{#{EXPR}}`), allowing injection of arbitrary Liquidsoap code. Commit `ff49ef4` migrated most user-controlled fields to the safe `toRawString()` method but left the remote relay password field using the vulnerable `cleanUpString()`. A user with the `RemoteRelays` station permission can achieve arbitrary code execution in the Liquidsoap process, leak internal API keys, or disrupt station operation.DetailsThe Vulnerable Sanitizer`cleanUpString()` at `backend/src/Radio/Backend/Liquidsoap/ConfigWriter.php:1349-1367`:The `/U` (ungreedy) flag causes `.*` to match the **minimum** characters until the first `}`. With nested input `#{#{EXPR}}`:Regex finds `#{` at position 0Ungreedy `.*` matches `#{EXPR` (stops at the **first** `}`)Full match consumed: `#{#{EXPR}` — replacement with capture group `$1` yields: `#{EXPR`The trailing `}` is appended by the regex engine (it was outside the match)**Final result: `#{EXPR}`** — a valid Liquidsoap string interpolation expressionThe Incomplete PatchCommit `ff49ef4` ("Use raw strings for user-input strings to avoid interpolation", 2026-03-06) correctly migrated host, username, mount, name, description, genre, and URL fields to `toRawString()`. However, the password field was left using `cleanUpString()`:`ConfigWriter.php:1208-1215`:The password is embedded in a Liquidsoap **double-quoted string**, which evaluates `#{...}` interpolation expressions.Why toRawString() Is Safe`toRawString()` uses Liquidsoap raw string delimiters (`{str_xxxxx|...|str_xxxxx}`) which **do not perform interpolation**, making them immune to this attack class.The Input PathAttacker sends `PUT /api/station/{station_id}/remote/{id}` with `source_password` containing the nested payloadEntity setter truncates to 100 chars via `mb_substr` (payloads fit within this limit)No validation on password contentOn station config regeneration, `ConfigWriter::getOutputString()` calls `cleanUpString()` on the passwordBypass produces valid interpolation, embedded in double-quoted Liquidsoap stringLiquidsoap evaluates the interpolation when loading the configPoCStep 1: API Key Disclosure (38 chars)After `cleanUpString()` processing, the password becomes `#{settings.azuracast.api_key()}`.When Liquidsoap loads the config, the generated line:evaluates to the internal API key value, which is then sent as the password to the remote relay server — observable by the attacker if they control the relay endpoint.Step 2: Remote Code Execution (54 chars)After processing: `#{process.run(string.char(105)^string.char(100))}` → executes `id` command.`string.char()` and the `^` concatenation operator are used to build the command string without double quotes (which `cleanUpString` replaces with single quotes, and Liquidsoap doesn't support single-quoted strings).Step 3: Trigger config regenerationRestart the station or modify any station setting to force Liquidsoap config regeneration. The payload executes when Liquidsoap loads the new config.The same bypass works with `$($(EXPR))` via the second regex `/\$\((.*)\)/U`.Impact**Arbitrary code execution** within the Liquidsoap process container via `process.run()`**Internal API key disclosure** via `settings.azuracast.api_key()`, granting the attacker full internal API access to the station**File read/write** within the Liquidsoap container via Liquidsoap's file operations**Station disruption** — malicious config can crash the Liquidsoap process**Low privilege bar** — requires only the `RemoteRelays` station permission, not global adminRecommended FixReplace `cleanUpString()` with `toRawString()` for the password field, consistent with the fix applied to all other fields in commit `ff49ef4`. The Shoutcast suffix append needs adjustment to work with raw strings:This uses the raw string delimiter which prevents all interpolation, matching the approach already used for host, username, mount, and all other user-controlled fields.Additionally, consider removing `cleanUpString()` entirely or marking it as deprecated, since `toRawString()` is the correct approach for all Liquidsoap string values. Any remaining callers should be migrated.
CVSS v3.1 score: 8.8