Skip to content
Injection Reviewed 2026-09-12

Command Injection

What does this mean ?

OS command injection occurs when untrusted data changes a command executed by the application. A shell command string is especially dangerous because the shell interprets its syntax. Even without a shell, a client-controlled executable or command-line option can perform an unintended action: this is often argument injection.

What can happen ?

An attacker may gain the ability to run operations with the application's OS permissions. An allowed executable can still read or change files, contact other systems or consume resources if its arguments are unrestricted.

Recommendation

Use a library function when possible: compress data using the language's compression API, for example. If an external utility is required, choose a fixed executable, pass arguments separately, allow only the business inputs you need, and set time/output limits. Do not allow the request to select a shell, script path or arbitrary flags.

An argument array prevents shell parsing but does not make every argument safe. Use -- only when the chosen utility documents support for it. Validate filenames and ownership separately. Java's SecurityManager is permanently disabled in JDK24; it is not a current command-injection control.

Sample Code

The examples use an application-owned report-tool utility. It is an illustrative dependency, not an installed command. Its reviewed contract accepts exactly --format followed by json or csv. The absolute executable path and trusted containing directory are configuration owned by the deployment. Adapt this pattern to your actual utility's documented options. Never execute the unsafe fragment.

# Unsafe
subprocess.run("/opt/app/bin/report-tool --format " + requested_format, shell=True)
import subprocess

def make_report(requested_format: str) -> None:
    if requested_format not in {"json", "csv"}:
        raise ValueError("Unsupported format")
    subprocess.run(
        ["/opt/app/bin/report-tool", "--format", requested_format],
        shell=False, check=True, timeout=10,
        stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
    )
// Unsafe
exec('/opt/app/bin/report-tool --format ' + requestedFormat);
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
const runFile = promisify(execFile);

async function makeReport(requestedFormat) {
  if (!['json', 'csv'].includes(requestedFormat)) throw new Error('Unsupported format');
  await runFile('/opt/app/bin/report-tool', ['--format', requestedFormat], {
    shell: false, timeout: 10_000, maxBuffer: 64 * 1024
  });
}

.NET ProcessStartInfo.ArgumentList keeps the two arguments separate:

if (requestedFormat is not ("json" or "csv"))
    throw new ArgumentException("Unsupported format");
var start = new System.Diagnostics.ProcessStartInfo
{
    FileName = "/opt/app/bin/report-tool",
    UseShellExecute = false
};
start.ArgumentList.Add("--format");
start.ArgumentList.Add(requestedFormat);
using var process = System.Diagnostics.Process.Start(start)
    ?? throw new InvalidOperationException("Could not start report tool");
if (!process.WaitForExit(10_000))
{
    process.Kill(entireProcessTree: true);
    throw new TimeoutException("Report tool timed out");
}
if (process.ExitCode != 0) throw new InvalidOperationException("Report tool failed");
if (!java.util.Set.of("json", "csv").contains(requestedFormat)) {
    throw new IllegalArgumentException("Unsupported format");
}
Process process = new ProcessBuilder(
    "/opt/app/bin/report-tool", "--format", requestedFormat)
    .redirectOutput(ProcessBuilder.Redirect.DISCARD)
    .redirectError(ProcessBuilder.Redirect.DISCARD).start();
if (!process.waitFor(10, java.util.concurrent.TimeUnit.SECONDS)) {
    process.destroyForcibly();
    throw new java.io.IOException("Report tool timed out");
}
if (process.exitValue() != 0) throw new java.io.IOException("Report tool failed");

PHP 7.4+ supports an array command for proc_open. This fragment demonstrates argument separation; enforce a process deadline in the calling job worker and constrain its output.

if (!in_array($requestedFormat, ['json', 'csv'], true)) {
    throw new InvalidArgumentException('Unsupported format');
}
$spec = [0 => ['file', '/dev/null', 'r'],
         1 => ['file', '/dev/null', 'w'],
         2 => ['file', '/dev/null', 'w']];
$process = proc_open(
    ['/opt/app/bin/report-tool', '--format', $requestedFormat], $spec, $pipes
);
if (!is_resource($process)) throw new RuntimeException('Could not start report tool');
if (proc_close($process) !== 0) throw new RuntimeException('Report tool failed');

The /dev/null paths make this example Unix-specific. Do not substitute a client-controlled program name.

if requestedFormat != "json" && requestedFormat != "csv" {
    return errors.New("unsupported format")
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, "/opt/app/bin/report-tool", "--format", requestedFormat)
return cmd.Run()

Imports: context, errors, os/exec, time. Do not turn this into exec.Command("sh", "-c", userInput).

raise ArgumentError, 'Unsupported format' unless %w[json csv].include?(requested_format)
ok = system('/opt/app/bin/report-tool', '--format', requested_format,
            out: File::NULL, err: File::NULL)
raise 'Report tool failed' unless ok

Run this in a worker with an enforced process deadline. Passing separate arguments avoids shell interpolation; an arbitrary user-selected executable would still be unsafe.

Regression checks

Replace the process runner with a stub. For json and csv, assert the executable and exact argument array. Reject an unknown format, whitespace, an option-like value and an empty value before the stub is called. Test timeout and nonzero exit handling using a harmless local fixture. This verifies the boundary without executing user-controlled commands.

References