You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Fix OS command injection in Local::start()/stop()/isRunning() (CWE-78, CWE-88)
Every caller-supplied value reached shell_exec()/system() by way of raw string
interpolation, so any consumer that forwards untrusted input into
Local::start() -- a wrapping HTTP service, a CI orchestrator splicing a
repo-scoped variable into localIdentifier or proxyHost, a multi-tenant test
runner -- handed the caller arbitrary command execution with the privileges of
the PHP process.
Confirmed reachable on eight distinct sinks: localIdentifier, proxyHost/Port/
User/Pass, hosts, logfile (both the -logFile argument and the truncating
system() call in start()), an arbitrary argument NAME through the add_args()
else-branch, the value side of that same branch, the public $pid property in
isRunning(), and the localIdentifier fragment reused by stop_command(). The
assembled line starts with the `exec` builtin, so a trailing `; cmd` chain does
not detonate -- but command substitution is expanded before exec runs, and
$(...) fires on all of them.
- add_args() rejects any argument name outside [A-Za-z0-9_-]+ with a
LocalException. A name is emitted as a `-<name>` flag, so it cannot be
quoted without ceasing to be a flag; it has to be validated instead. The
charset keeps every documented custom flag working, dashes included.
- Every caller-supplied value is wrapped in escapeshellarg() -- available
since PHP 4, so the declared php >= 5.3.19 floor is untouched.
- isRunning() casts $pid to int and reports a non-integer pid as not running
instead of asking ps about it.
- start_command()/stop_command() assemble a filtered list of parts rather
than interpolating one string and collapsing whitespace afterwards. That
collapse only existed to squeeze out the gaps left by unset flags, and it
rewrote whitespace inside quoted values too, which would now corrupt
legitimately escaped arguments.
- start()'s logfile truncation is quoted as well; its Windows branch used a
single-quoted PHP string, so it had been truncating a file literally named
'$this->logfile' rather than the configured one.
- `$call . "2>&1"` was missing its separating space; it only worked because
the old whitespace collapse left a trailing one.
Values now reach the binary as single quoted argv elements, which is what the
binary already received for benign input -- no behavioural change there. The
emitted command line does change shape (values are quoted), so the tests that
assert on it are updated.
Tests: eight injection regression tests that execute the assembled command line
for real against /bin/echo and assert the payload never runs. All eight fail on
the pre-fix code. tests/manual/injection-poc.php is the same proof as a
standalone script (8 of 9 arms vulnerable before, 0 after).
The test harness is modernised to phpunit ^9.6 with a CI workflow, matching the
open TLS-verification PR, because phpunit 4.6 cannot boot on a supported PHP and
the regression tests would otherwise never execute.
Residual, deliberately not in scope: the access key is still a positional
argument and so is still visible in `ps`/`/proc/<pid>/cmdline`. It can no longer
inject, and moving it off the command line needs binary-side support -- tracked
separately.
0 commit comments