GitHub Security Advisory: GHSA-4fm3-ggg2-c6qx
Package: azuracast/azuracast (COMPOSER)
Severity: MODERATE (CVSS 6.3)
CWE: CWE-862: Missing Authorization
Vulnerable versions: <= 0.23.5
Patched in: 0.23.6

Summary: AzuraCast's Missing RequireInternalConnection on Liquidsoap API Allows Low-Privilege Metadata Injection and Broadcast Disruption

Details:
## Summary

The `/api/internal/{station_id}/liquidsoap/{action}` endpoint is accessible from the public web interface because it lacks the `RequireInternalConnection` middleware that protects other internal endpoints (`/sftp-auth`, `/sftp-event`). Combined with a logic flaw where the `$asAutoDj` flag is set based on the *presence* of the `X-Liquidsoap-Api-Key` header rather than its *validated value*, any user with the basic `View` station permission can invoke privileged Liquidsoap commands — injecting arbitrary now-playing metadata visible to all listeners, disrupting live broadcast tracking, and disclosing absolute filesystem paths.

## Details

**Issue 1: Missing RequireInternalConnection middleware**

In `backend/config/routes/api_internal.php`, the liquidsoap route group (lines 17-21) lacks the `RequireInternalConnection` middleware:

```php
// Lines 17-21 — NO RequireInternalConnection
$group->map(
    ['GET', 'POST'],
    '/liquidsoap/{action}',
    Controller\Api\Internal\LiquidsoapAction::class
)->setName('api:internal:liquidsoap');
```

Compare with sftp endpoints that correctly apply it:

```php
// Lines 32-34 — HAS RequireInternalConnection
$group->post('/sftp-auth', Controller\Api\Internal\SftpAuthAction::class)
    ->setName('api:internal:sftp-auth')
    ->add(Middleware\RequireInternalConnection::class);
```

The nginx config (`util/docker/web/nginx/azuracast.conf.tmpl`) only sets the `IS_INTERNAL` FastCGI parameter on the internal port 6010 listener (line 44), not on the public-facing server block (ports 80/443). Without the middleware, the endpoint is fully accessible from the public internet.

**Issue 2: `$asAutoDj` derived from header presence, not validated value**

In `backend/src/Controller/Api/Internal/LiquidsoapAction.php`:

```php
// Line 34 — checks header PRESENCE, not value
$asAutoDj = $request->hasHeader('X-Liquidsoap-Api-Key');

// Lines 38-44 — key value only checked when ACL FAILS
$acl = $request->getAcl();
if (!$acl->isAllowed(StationPermissions::View, $station->id)) {
    $authKey = $request->getHeaderLine('X-Liquidsoap-Api-Key');
    if (!$station->validateAdapterApiKey($authKey)) {
        throw new RuntimeException('Invalid API key.');
    }
}
```

When a user authenticates via session/API key and has `StationPermissions::View`, the ACL check passes and the adapter API key is never validated. But `$asAutoDj` is already `true` from line 34 because the header is present (with any arbitrary value).

**Affected commands:**

- `FeedbackCommand` (`backend/src/Radio/Backend/Liquidsoap/Command/FeedbackCommand.php:36`): Guard `if (!$asAutoDj) return false;` bypassed — creates SongHistory records and forces NowPlaying cache updates
- `DjOffCommand` (`backend/src/Radio/Backend/Liquidsoap/Command/DjOffCommand.php:24`): Guard bypassed — calls `$this->streamerRepo->onDisconnect($station)` which ends all active broadcasts and sets `$station->is_streamer_live = false`
- `DjOnCommand` (`backend/src/Radio/Backend/Liquidsoap/Co