Skip to content

UntrustedShellCommand ​

error native on by default

Group: Security

Detect request-derived data reaching a shell-command sink in syntax-capable position without proven appropriate argument escaping or strict validation.

Example ​

php
<?php
system('viewer ' . $_GET['path']);
$listing = `ls {$_GET['dir']}`;
proc_open('convert ' . $_GET['file'], [], $pipes);

// Not reported: the request only selects a constant, or contributes a number.
$mode = match ($_GET['mode']) { 'full' => '--full', default => '--quick' };
system('backup ' . $mode);
system('head -c ' . strlen($_POST['body']) . ' /dev/zero');
proc_open(['convert', $_GET['file']], [], $pipes);

Reported:

  • line 2: Keep untrusted input out of shell command syntax.
  • line 3: Keep untrusted input out of shell command syntax.
  • line 4: Keep untrusted input out of shell command syntax.

Detection ​

  • Report request-derived data reaching a shell-command sink in syntax-capable position without proven appropriate argument escaping or strict validation.
  • Shell-command sinks are the first (command) argument of resolved builtin system, exec, shell_exec, passthru, popen and proc_open, and the text of a backtick command expression, which PHP runs exactly like shell_exec. A call to a project function is a sink when its summary forwards an argument, or request data it reads itself, to one of these sinks: proc_open and a backtick command count there like the other four, except a proc_open the function gives an argument vector (E1).
  • Decide per operand whether request data can become command text. Concatenation, interpolation and ?? carry any of their operands; a conditional or match expression carries only its result operands, never its condition or subject; a variable with a single reaching assignment carries what that assignment carries. Every other request-derived operand carries its data unless E1 or E3 applies. A variable is followed to its reaching assignment only when its scope cannot rebind it behind that assignment; a reference alias, a by-reference closure capture, foreach or argument, a catch binding or static declaration of the same name, extract(), an assignment through a variable variable, a global declaration of it or a $GLOBALS access that is not a different literal entry keeps the report.
  • Resolve builtin calls and named arguments before checking the contract. Project-local callees are considered only when their bodies or immutable summaries establish the required facts. The default is enabled, with positive proof required.
  • For flow-dependent conditions, require a reachable source-to-use path with no intervening invalidation. Unknown calls, ambiguous aliases, recursion or analysis-budget exhaustion cannot establish definite facts.

Project method wrappers are followed only when a single visible declaration is fixed by a final receiver class, a final method, or a private method accessible at the call. Open overridable methods, ambiguous declarations, conditional class bodies and trait bodies do not establish a wrapper sink. Static calls must name a static method. These contracts compose through nested project wrappers and refresh when class or method dispatch metadata changes.

A wrapper dependency that reads only array values preserves the numeric safety of an array whose values are all numbers, booleans or null. Array keys, whole-array formatting and mixed key/value dependencies remain untrusted; numeric conversion of extracted keys can establish safety again. Return and reference-output dependencies preserve the same distinction.

Exceptions (no report) ​

  • E1. Resolved argument-vector execution, escaped arguments with validated option policy, constant commands and unknown flows are excluded. proc_open whose command is an array literal, a variable holding one or an array-typed expression is argument-vector execution. A command assembled only from constants selected by request data (for example match arms or conditional branches that are all literals) is a constant command.

  • Unresolved targets, incomplete required facts, suppressed findings and PHP versions lacking the referenced API or language feature produce no report. User declarations shadowing builtin names are not builtin contracts.

  • E3. Known boolean or numeric scalar output and resolved builtin md5, sha1, hash or hash_hmac hexadecimal output cannot inject the target text syntax. Raw binary or unknown output modes retain source taint. These narrow text proofs do not establish URL or filesystem-path authorization.

  • E4. Under E3, numeric scalar output covers: integer, float and boolean casts; the results of -, *, /, % and ** and of unary + and -, which yield a number or fail; a resolved builtin call whose declared result on the configured PHP version consists only of integer, float, boolean and null (such as strlen, count or strpos); and + when at least one operand is a literal or such a builtin call. + between two operands that may both be arrays is an array union that keeps request-derived elements and is not excluded. Bitwise operators on strings produce strings and are not excluded.

  • A wrapper call is not reported when every request-derived argument written at the call is excluded by E1, E3 or E4.

  • Request data that a switch compared with constant labels is the matched label inside an arm reached only through string labels that do not read as numbers (not empty and not starting, after whitespace, with a digit, a sign or a dot), and after the switch when every other way out replaced it or left the scope: switch ($sort) { case 'name': case 'date': break; default: $sort = 'name'; } validates $sort. A numeric, empty, boolean or null label, a label expression, a fall-through from another arm, a default arm and any other subject prove nothing. The subject is a plain variable or an element of a request superglobal read under one constant key (switch ($_GET['action']), not $_FILES); the element is the matched label only while nothing that may run code is evaluated: a call, an object creation, an assignment, an unset, or the use of a local variable, property, offset of a local or class constant ends it, as does a reference to the superglobal. A switch (true) reads each case expression like the condition of an if/elseif chain.

Fix ​

No automatic fix: executable, option boundaries and allowed arguments require explicit policy.

PHP versions ​

Apply only on PHP versions supporting the referenced APIs and syntax (within PHP 5.3–8.5). Builtin behavior and argument contracts follow the configured PHP version. Extension-specific calls require resolved extension API symbols; do not treat same-named application functions as extension calls.

Options ​

None. Enable or disable this rule through normal rule configuration.

Configure ​

In custos.json:

json
{
  "rules": {
    "UntrustedShellCommand": {
      "enabled": false
    }
  }
}

Suppress ​

Before the statement or declaration (or the first statement of the file):

php
// @custos-ignore UntrustedShellCommand

Released under the MIT License. Native rules and rules modeled on Php Inspections (EA Extended); independent clean-room implementation.