Choose your stack

Node.js · Express

Fix OS command and argument injection in Node.js and Express

Run the program directly with its arguments as a list - execFileSync(tool, ['--', name]) - so no shell parses them, and put '--' before any user operand so a leading dash cannot pose as an option.

Open the fix card
Python · Flask

Fix OS command and argument injection in Python and Flask

Pass a list to subprocess.run with the default shell=False - subprocess.run([tool, '--', name]) - so no shell parses the arguments, and put '--' before any user operand so a leading dash cannot pose as an option.

Open the fix card
Java · Spring Boot

Fix OS command and argument injection in Java and Spring Boot

Build the process from a List so ProcessBuilder runs it directly - new ProcessBuilder(List.of(tool, "--", name)) - and no shell parses the arguments; the "--" makes a leading-dash operand data, not an option.

Open the fix card
C# · ASP.NET Core

Fix OS command and argument injection in C# and ASP.NET Core

Start the process with ProcessStartInfo.ArgumentList and UseShellExecute = false - psi.ArgumentList.Add("--"); psi.ArgumentList.Add(name) - so no shell parses the arguments, and the '--' makes a leading-dash operand data, not an option.

Open the fix card
TypeScript · Next.js

Fix OS command injection in TypeScript and Next.js

Run the program directly with its arguments as a list, execFileSync(tool, ['--', name]), so no shell parses them, and put '--' before any user operand so a leading dash cannot pose as an option.

Open the fix card
Go · net/http

Fix OS command and argument injection in Go and net/http

Run the program itself with each argument as its own string: exec.CommandContext(ctx, tool, "--", name). os/exec never invokes a shell unless you call sh -c, so metacharacters stay characters, and "--" keeps a name that starts with a dash from posing as an option.

Open the fix card

Stacks without a card

How it happens

  • The code builds a command string from user input and hands it to a shell (Node child_process.exec, Python subprocess with shell=True, Java sh -c, C# /bin/sh -c). The shell, not the program, parses the string.
  • Shell metacharacters start new work: ; and a newline separate commands, | pipes into another, && and || chain, and $( ) and backticks substitute a command's output. Any one of them turns the input into extra commands that run with the app's privileges.
  • Argument injection is the quieter route: even with no shell, passing an operand that begins with a dash lets it pose as an option. A value like --output=/path or -o /path makes a tool write a file the caller never intended, and tools such as git, tar and find have far more dangerous flags.
  • Hand-made fixes fail. Wrapping a value in double quotes inside a shell string still lets $( ) and backticks run, a metacharacter blocklist always misses one (newlines, globbing, or the leading dash it was never about), and quoting breaks on a name that itself contains a quote.

Fix principles

  • Do not call a shell. Run the program directly and pass each argument as its own element of a list (execFile/spawn, subprocess.run([...]) without shell=True, ProcessBuilder(List), ProcessStartInfo.ArgumentList), so the OS never reparses your data.
  • End option parsing with a "--" argument before any user-supplied operand, so a value that starts with a dash is treated as data, not as a flag.
  • Allowlist anything that selects part of the command itself - the program name, a subcommand, a flag or an enumerated option value - and reject everything else with a 400; a value cannot be passed through as free text.
  • Validate the operands you do pass: constrain type, length and character set to what the feature actually needs.
  • Do not escape or blocklist your way out of a shell. Quoting and metacharacter filters depend on the shell dialect and on predicting every payload, and they do nothing about argument injection.
  • Run the tool with least privilege and keep its stderr away from clients, so a call you missed does less damage and leaks less.

How the cards prove it

  • Run each card's tests in three modes: the fixed implementation must pass, and the vulnerable and the plausible-but-wrong implementations must fail. run-all.mjs does this for every card.
  • The exploit tests send a separator, a pipe, a chain, a command substitution, backticks and a newline, plus an argument-injection value that starts with a dash, and require that a harmless marker file was never created. Functional tests keep names with spaces, apostrophes, double quotes and non-ASCII characters working.
  • Static analysis helps but is uneven: Bandit B602 and Semgrep's subprocess-shell-true and ProcessBuilder rules flag the shell call, but Semgrep's Node rule misses the node:child_process import, Semgrep CE has no C# rule, CA3006's taint pass did not activate, and none of them model argument injection - so the marker-file regression tests are the real control (each card's scannerRules records exactly what fired).

References

  1. OWASP OS Command Injection Defense Cheat Sheet
  2. OWASP Top 10:2025 A05 Injection
  3. CWE-78: Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')
  4. CWE-88: Improper Neutralization of Argument Delimiters in a Command ('Argument Injection')
  5. CAPEC-88: OS Command Injection
  6. CAPEC-6: Argument Injection
  7. PortSwigger Web Security Academy: OS command injection

Attack patterns:

Practice in the browser

Guides on this weakness

Guide

Keep untrusted values out of executable syntax

Choose a safe interface for each interpreter and bound the work performed by parsers and converters.

Guide

Run external programs without command or argument injection

Run programs from an argument list with no shell, end options before user values, and keep untrusted arguments away from Windows batch files.

Guide

Keep untrusted CI work outside the release boundary

Review workflow authority, persistent state and artifact handoffs before contributor-controlled code reaches privileged jobs.

Guide

ASVS 5.0 verification records: requirement, test and evidence another engineer can repeat

Scope edition-qualified requirements, connect them to implementation and tests, and separate a passing check from an unsupported assurance claim.

Guide

Keep generated output out of executable contexts

Choose safe rendering and narrow operations so model text cannot become browser code, database syntax or an unrestricted command.

Guide

Apply CISA Secure by Design: principles, pledge goals and evidence

What CISA's Secure by Design guidance and pledge ask of software manufacturers as of October 2026, and how to turn each of the three principles, the seven pledge goals and the Product Security Bad Practices into engineering actions, metrics and evidence.

Case study

Case study: the Nx s1ngularity compromise and its AI-assisted payload (2025)

How a pull-request-title injection in a GitHub Actions workflow on an outdated branch gave an attacker the Nx npm token in August 2025, how the malicious versions tried to turn developers' own AI command-line tools into file-search agents, and the workflow, publishing and agent controls that stop the same chain.