GHSA-xw67-cg5f-4m2r: AVideo: OS command injection in on_publish.php execAsync via unescaped m3u8 URL
Summary**Type:** Classic shell-metacharacter injection. The YPTSocket notification branch in `plugin/Live/on_publish.php` builds an `execAsync()` command line by string concatenation, single-quoting each argument but never calling `escapeshellarg()`. A `'` in any of the three interpolated values (`$users_id`, `$m3u8`, `$obj->liveTransmitionHistory_id`) closes the quoted token and lets the attacker append arbitrary commands.
**File:** `plugin/Live/on_publish.php`, line 267.
**Root cause:** the developer wrapped each variable in literal single quotes (`'$users_id'`, `'$m3u8'`, `'$obj->liveTransmitionHistory_id'`) believing this provides shell-quoting. PHP single-quoted-into-shell is not safe quoting; it is just two literal quote characters that the shell pairs greedily. Any embedded `'` closes the outer string and resumes interpretation in the shell. The rest of the AVideo codebase already calls `escapeshellarg()` (137 call sites across the project) for ffmpeg invocations, so the safe primitive is well-known to the project; it was simply omitted from this branch. The endpoint is web-reachable (no `.htaccess` rule restricts `on_publish.php`, no `REMOTE_ADDR` check), so the trigger is a direct HTTP POST without going through nginx-rtmp.Affected Code**File:** `plugin/Live/on_publish.php`, lines 256-271.`Live::getM3U8File($key, false, true)` (`Live.php:1337-1350` -> `Live.php:4845-4889`) returns `"{$playerServer}{$uuid}.m3u8"` (or `"{$playerServer}{$uuid}/index.m3u8"`) where `$uuid = $this->getKeyWithIndex(...)` is the stream key string read straight out of the `live_transmitions` table. There is no character normalisation between database read and command construction.**Why it's wrong:** `'$m3u8'` is not shell quoting. PHP interpolates `$m3u8` into the string between two literal `'` characters. The shell then tokenises the result. If `$m3u8` contains `'` itself, the shell sees `'…'` followed by `<attacker bytes>` followed by another `'`, which forms two adjacent quoted strings concatenated with whatever the attacker put between them. Embedded `;`, backticks, `$()`, `&&`, `|`, or `\n` then run as shell commands. The fix is `escapeshellarg()`, which AVideo already uses 137 times in ffmpeg invocations (e.g. `getVideos.php:1069`, `videos.json.php`, `aVideoEncoder.json.php`); this branch simply forgot it.Exploit ChainAttacker authenticates and arranges for one of the command variables to contain `'`. Under the current code the readily available primitive is a `canStream` user supplying a stream key via the persistence path (`saveLive.php`'s `$_REQUEST['key']` is written verbatim to `live_transmitions.key`). State: a row exists with `key = "evilkey';id>/tmp/pwn;#"`.Attacker POSTs directly to `https://target/plugin/Live/on_publish.php` (the file is web-served, no IP restriction) with body:`on_publish.php:117` runs `preg_replace("/[&=]/", '', $_POST['name'])` — only `&`/`=` are stripped, so `';id>/tmp/pwn;#` survives. Lines 143-163 confirm `$_GET['p'] === $user->getPassword()` (the attacker is themself, knows their own MD5), persist a `LiveTransmitionHistory` row with the poisoned key, and set `$obj->error = false`. State: authorisation gate passed.Line 261 calls `Live::getM3U8File($lth->getKey(), false, true)`, returning `"https://server/live/evilkey';id>/tmp/pwn;#.m3u8"`. State: `$m3u8` carries the injection payload.Line 267 builds the command string by concatenation:Shell tokenisation sees: `php`, `…/on_publish_socket_notification.php`, `'7'`, `'https://server/live/evilkey'` (the attacker's `'` closed the second quote), then operator `;`, then command-2 `id>/tmp/pwn`, then `;`, then `#.m3u8' '42'` (everything after `#` is a comment). State: the shell has parsed two real commands.Line 269 `execAsync($command)` spawns the shell, which runs the secondary command `id>/tmp/pwn` as the AVideo PHP-FPM/Apache user. State: arbitrary OS command execution with the privileges of the web-server runtime user.Final state: the attacker reads `/tmp/pwn`, swaps the payload for a reverse shell, exfiltrates `videos/configuration.php` (database password and root URL), drops a webshell into the upload tree, or pivots to other plugin credentials (PayPal/Stripe API keys, AWS keys for the CDN plugin, OpenAI key for the AI plugin).Security Impact**Severity:** sec-high. Pre-auth-friendly remote code execution: the only prerequisite is that the attacker can place a `'` into one of the three command-line variables, which on a streaming platform means a single low-privilege account.
**Attacker capability:** with one `canStream` account and two HTTP requests, the attacker executes arbitrary shell commands as the AVideo runtime user. From there: read database credentials, exfiltrate user data, write a webshell into a publicly-served path, pivot to plugin credentials, persist via cron, or escalate via any local sudoers entries.
**Preconditions:** AVideo deployment with `Live` and `YPTSocket` plugins enabled (the standard live-streaming bundle); attacker can reach `/plugin/Live/on_publish.php` over the network; a value containing `'` is reachable into `users_id`, `m3u8`, or `liveTransmitionHistory_id` (the current code lets `canStream` users supply such a value via the stream-key persistence path).
**Differential:** source-inspection-verified end-to-end. The shell-tokenising behaviour of `'…'…'…'` is reproducible offline:The patched build (with the suggested `escapeshellarg()` fix below applied) constructs `php /a/b.php '7' 'https://s/live/evilkey'\''id>/tmp/pwn;#.m3u8' '42'`, which the shell parses as a single argument containing the literal characters; the second command never runs.Suggested FixUse `escapeshellarg()` on every variable interpolated into the command string. This matches established project conventions (137 other call sites for ffmpeg invocations).Defence-in-depth: `on_publish.php` is the nginx-rtmp webhook and should not be reachable from the public Internet. Add an `.htaccess`/nginx `location` rule restricting the file to `127.0.0.1` and any configured RTMP server IPs. That blocks the trigger path independently of the sanitisation work.
CVSS v3.1: 8.8