Join our Newsletter — 33% off our NHI Course
Home› Guides› Command Injection Guide: Preventing OS Command Execution Flaws
Guide Web & API Security

Command Injection Guide: Preventing OS Command Execution Flaws

PDF for newsletter subscribers · free to read online

← All guides
By Lalit Choda, NHI Mgmt Group Updated 24 October 2026 13 min read
On this page

OS command injection happens when an application builds an operating system command from untrusted input and the shell, or the target program, interprets part of that input as instructions. It remains heavily exploited: in July 2024 CISA and the FBI issued a Secure by Design alert after edge-device command injection flaws in Cisco NX-OS, Palo Alto Networks PAN-OS and Ivanti Connect Secure were used in active campaigns. This guide explains how the flaw works, where it hides beyond web forms (CI/CD workflows, appliances, AI agent tools), how to detect it and how to remove the class from your code. It is for developers, AppSec engineers and detection teams, and contains no payloads. For the wider family of web attacks, see the Web Application Attacks Guide.

Key takeaways

  • The fix is structural, not a filter. Call a library function instead of a program, or pass the program and each argument as separate list elements with no shell. Escaping and denylists are the weakest layers, not the main defence.
  • Every command injection is also argument injection. Removing the shell stops metacharacters, but a value that starts with a dash can still become an option. Validate arguments against an allowlist and end option parsing where the program supports it.
  • It is not only a web form problem. Branch names in CI workflows, filenames, hostnames in network appliance diagnostics and parameters chosen by AI agents all reach shells.
  • Impact equals the privileges of the process. Injected commands run as the service account, read its secrets and tokens, and move from there. Least privilege and secret hygiene decide how bad a single flaw becomes.
  • Detect at the process level. A web server or application runtime spawning a shell or a download utility is one of the highest-value detections you can build, and it catches flaws you have not found yet.

How the attack works

MITRE classifies the weakness as CWE-78, Improper Neutralization of Special Elements used in an OS Command, a Base weakness whose parent is CWE-77 (Command Injection) and which can also be CWE-88 (Argument Injection). The product builds all or part of a command from externally influenced input and fails to neutralise elements that change the meaning of that command when it reaches the downstream component. CWE-78 has appeared in every CWE Top 25 list from 2019 to 2025. See the CWE Top 25 Guide.

There are two distinct failure modes, and defences differ for each:

  • Shell interpretation: the application hands a single string to a shell (sh -c, cmd.exe /c, powershell.exe). The shell parses separators, substitutions, redirections and quoting rules, so data can become a second command. CAPEC records this as CAPEC-88 OS Command Injection, with CAPEC-15 Command Delimiters as a common variant.
  • Argument injection: no shell is involved, but the program itself treats a value as an option. A value that begins with a dash, or an option that writes files or loads configuration, can change behaviour with no metacharacters at all. CAPEC records this as CAPEC-6 Argument Injection. The OWASP OS Command Injection Defense Cheat Sheet states that every OS command injection is also an argument injection.

Blind command injection is the same flaw where output is not returned to the user. Success is inferred from timing or out-of-band network calls, so egress controls and DNS logging matter.

Why the language API matters

LanguageRisky patternSafer patternSource note
Pythonsubprocess with shell=True, os.systemsubprocess.run([...]) with a list and no shellThe subprocess docs state the library never calls a shell implicitly; with shell=True quoting is the application's responsibility. On Windows, .bat and .cmd files may still be run by the system shell.
Node.jschild_process.exec(), execSync(), any call with shell: trueexecFile() or spawn() with an argument arrayThe docs warn never to pass unsanitised input to exec(). Passing args with shell: true is deprecated (DEP0190) from v22.15.0 and v23.11.0.
JavaRuntime.exec(String), or ProcessBuilder wrapping sh -cProcessBuilder or Runtime.exec(String[]) with separate elementsOWASP notes Runtime.exec(String) is deprecated since Java 18 and splits on whitespace.
.NETBuilding ProcessStartInfo.Arguments from input, UseShellExecute = trueFully qualified path, UseShellExecute = false, ArgumentListOWASP: never pass untrusted input to cmd.exe, powershell.exe or sh -c.
PHPsystem(), exec(), shell_exec(), backticks with concatenated inputAvoid; if unavoidable, hardcode the command and wrap each value with escapeshellarg()OWASP prefers escapeshellarg(); escapeshellcmd() still allows extra parameters.

Where it shows up

  • Network and edge appliances: diagnostic pages (ping, traceroute, DNS lookup), certificate and log utilities and management APIs often wrap shell scripts. The CISA and FBI alert of July 2024 named CVE-2024-20399 (Cisco NX-OS), CVE-2024-3400 (PAN-OS) and CVE-2024-21887 (Ivanti Connect Secure), all in CISA's Known Exploited Vulnerabilities catalog. In the local KEV snapshot used for this guide (catalog 2026.10.21), 112 entries are tagged CWE-78 and a further 33 CWE-77.
  • Web applications: file conversion (images, PDFs, office documents), archive handling, backup and export jobs, and wrappers around tools such as git, curl or ffmpeg. LaTeX rendering is a special case: OWASP ASVS 5.0 requirement V1.2.8 asks that LaTeX processors run without --shell-escape.
  • CI/CD workflows: GitHub documents that values such as github.event.pull_request.title, body, head_ref and ref are attacker-controllable, and that ${{ }} expressions are substituted into an inline run script before the shell executes it. That is command injection in a pipeline, running with the workflow's token and secrets. See the GitHub and GitHub Actions Security Guide and the CI/CD Security Guide.
  • Email and protocol handlers: SMTP and IMAP command injection is a related class where CRLF sequences in user input inject protocol commands rather than OS commands. Use a library that encodes fields rather than concatenating strings.
  • AI agents and tools: an agent with a shell tool, or an MCP server that wraps a CLI, turns model output into command arguments. Model output is untrusted input, and prompt injection can steer it. See the AI Agent Sandboxing and Code Execution Guide and the MCP Security Guide.

Impact

Injected commands run with the identity, file permissions and network reach of the vulnerable process. In practice that means:

  • Secret theft: environment variables, configuration files, cloud instance metadata credentials, CI tokens and API keys readable by the service account (ATT&CK T1552.001 Credentials In Files).
  • Persistence: web shells (T1505.003) and tools pulled in from outside (T1105 Ingress Tool Transfer).
  • Lateral movement: appliance-held service accounts and directory bind credentials give a path into the internal network. In the Ivanti Connect Secure exploitation of 2024, an authentication bypass chained with the CVE-2024-21887 command injection exposed user passwords, service accounts, stored API keys and certificates, and CISA ordered agencies to reset them.
  • Supply chain: in a pipeline, code execution can poison artefacts or publish releases.

Where identity fits: the blast radius is set by the non-human identity the process runs as. The same flaw is a breach under root with a long-lived cloud key, and a contained incident under a low-privilege identity with short-lived scoped credentials. See the Service Account Security Guide.

Detection

In code and pipelines

  • SAST rules on sinks: flag shell=True, os.system, exec(), Runtime.exec(String), system() and backticks, and any call that includes sh -c, cmd /c or powershell -Command. CWE notes automated static analysis can miss custom wrappers, so pair it with manual review.
  • Workflow scanning: GitHub recommends CodeQL, which can detect vulnerable Actions workflows. Add a rule that blocks ${{ github.event.* }} inside run steps.
  • DAST and fuzzing: time-based and out-of-band tests find blind cases that response-diffing scanners miss. See the Web Application Security Testing Guide.

At runtime

SignalWhere to collect itWhy it matters
Web server or app runtime (w3wp.exe, java, node, python, php-fpm, httpd) spawning sh, bash, cmd.exe or powershell.exeSysmon Event ID 1 or Windows Security event 4688 with command line; Linux auditd execve or EDR process telemetryCore signal for T1059 (T1059.004 Unix Shell, T1059.003 Windows Command Shell) launched from a public-facing application (T1190)
Service process starting download or recon tools (curl, wget, certutil, whoami, id, uname)EDR and process creation logsCommon post-injection activity
New executable or script files in web rootsFile integrity monitoring, EDR file eventsWeb shell drop
Unexpected outbound DNS or HTTP from application serversDNS resolver logs, egress proxy, VPC flow logsBlind injection confirmation and data exfiltration
CI job logs with unusual commands or network calls; releases without matching commitsCI audit logs, package provenancePipeline injection

MITRE D3FEND names the countermeasure D3-PSA Process Spawn Analysis. Baseline the child processes each service legitimately starts and alert on anything else. See the Sysmon Guide and the Detection Engineering Guide.

Prevention and hardening

Defence layers in order of strength

LayerWhat it doesLimits
1. Do not call the OSUse a library (image, archive, DNS, mail, git libraries) instead of shelling outNot always available; removes the class where it is
2. No shell, argument arrayProgram path and each argument passed separately; the shell never parses inputStops metacharacters, not argument injection
3. Allowlist validationCommand from a fixed list; each value matched in full (for example Python fullmatch) against a strict pattern or mapped from an IDNeeds per-parameter rules; denylists are not a substitute
4. Option hygienePlace -- before operands where supported; reject values starting with a dash; for curl, one validated URL and --globoffOWASP notes -- is not a complete fix on its own
5. Escapingescapeshellarg(), shlex.quote() where a shell is unavoidablePlatform-specific and fragile; last resort
6. Contain the processDedicated low-privilege account, read-only file system, seccomp or AppArmor profiles, no outbound internet, short-lived scoped credentialsLimits impact, does not remove the flaw

OWASP ASVS 5.0 requirement V1.2.5 captures layers 2 and 5: the application protects against OS command injection and operating system calls use parameterised OS queries or contextual command line output encoding. D3FEND countermeasures for layer 6 include D3-SCF System Call Filtering and D3-EAL Executable Allowlisting. See the Application Allowlisting Guide.

CI/CD specifics

  • Intermediate environment variables: GitHub calls this the preferred approach. Map the untrusted context to an env: variable and reference it, double-quoted, in the script, so the expression is never pasted into script text.
  • Prefer an action over inline script, so the value arrives as an argument.
  • Restrict GITHUB_TOKEN to read-only by default and raise permissions per job.
  • Avoid pull_request_target unless necessary, and never check out untrusted pull request code in privileged workflows.

Rollout plan

  1. Inventory sinks: run SAST and a grep across all repositories for process execution APIs and shell invocations, including CI workflow files and infrastructure scripts. Record owner and data source for each.
  2. Triage by exposure: fix first where input reaches the sink from unauthenticated or external sources, then authenticated users, then internal jobs.
  3. Refactor to layers 1 to 3, service by service, with unit tests asserting hostile values (metacharacters, leading dashes, long strings) are rejected or treated as literals.
  4. Turn on runtime detection in alert-only mode for shell spawns from each service, tune the baseline for two to four weeks, then raise severity. Seccomp or AppArmor profiles go to complain mode first, then enforce; rolling back is a profile change, not a code change.
  5. Add a CI gate that fails builds on new shell sinks without an approved exception, and on ${{ github.event.* }} in run steps.
  6. Verify with testing: scope command injection into penetration tests. See the Penetration Testing Guide.

Decision guidance

  • Small teams: layers 1 and 2, the CI rules, one SAST rule set and EDR process spawn alerts cover most risk cheaply.
  • Large or product organisations: follow the CISA approach of treating CWE-78 as a class to eliminate: review past defects, fund safe wrapper libraries that only accept argument arrays, ban raw shell APIs by lint rule.
  • Buying appliances: ask vendors how they have removed shell calls from management interfaces and whether they have signed the Secure by Design pledge; patch KEV-listed command injection flaws on edge devices first.

Measuring progress

  • Number of shell-invoking sinks in code, trending to zero, with each remaining one documented.
  • Percentage of services with a process spawn baseline and alerting enabled.
  • Mean time to remediate KEV-listed CWE-78 and CWE-77 flaws on internet-facing devices.
  • Percentage of CI workflows with read-only default token permissions and no untrusted expressions in run steps.

Framework mapping

FrameworkReferenceRelevance
NIST SP 800-53 Rev. 5SI-10 Information Input Validation; SA-11 Developer Testing and Evaluation; AC-6 Least Privilege; CM-7 Least Functionality; SI-4 System MonitoringValidation, testing, containment, detection
NIST CSF 2.0PR.PS-06 secure software development; DE.CM-09 runtime monitoringProgramme level
OWASP ASVS 5.0V1.2.5, V1.2.8Verification requirements
ISO/IEC 27001:2022Annex A 8.28 Secure codingPolicy anchor
MITRE ATT&CKT1190, T1059, T1505.003, T1105Detection engineering
MITRE D3FENDD3-PSA, D3-SCF, D3-EALCountermeasures

Lessons from incidents

  • Pipeline values are user input. In the Ultralytics PyPI compromise of December 2024, a branch name was inserted directly into a shell command in a pull_request_target workflow, giving code execution with CI privileges; malicious releases followed. A near-identical injection had been reported and fixed in August 2024, then reintroduced about ten days later, which is why a CI gate beats a one-off fix. A leftover long-lived PyPI token made a second round of releases possible.
  • Edge devices hold the keys. The Ivanti case shows that after command injection on an appliance, every secret on it must be treated as exposed. Keep an inventory of appliance-held service accounts and keys so rotation takes hours, not weeks.

Practitioner checklist

  • Inventory every process execution call and shell invocation across code, scripts and CI workflows, with an owner for each.
  • Replace shell calls with libraries where possible; elsewhere pass an argument array with no shell.
  • Validate each argument against a full-match allowlist or map it from a fixed ID; reject values starting with a dash.
  • Ban exec(), shell=True, os.system and Runtime.exec(String) by lint rule, with documented exceptions only.
  • Move untrusted CI context values into environment variables, set GITHUB_TOKEN to read-only by default and review every pull_request_target workflow.
  • Run services as dedicated low-privilege identities with short-lived scoped credentials and no outbound internet unless required.
  • Alert on web and application runtimes spawning shells or download tools, using a per-service baseline.
  • Patch KEV-listed command injection flaws on edge devices first and rotate appliance-held secrets after any compromise.
  • Add command injection, including blind and argument injection cases, to test plans for every wrapper and appliance.

Standards and references

Related NHI Mgmt Group resources: SQL Injection Prevention Guide · Path Traversal Guide · Server-Side Template Injection (SSTI) Guide · Secure Coding Guide

PDF for subscribers

Take this guide with you

Every section, table and checklist in one PDF, with links to the sources and related guides. Subscribe to our weekly NHI & AI Identity Journal and we’ll email you a download link.

Free weekly newsletter

Subscribe to the NHI & AI Identity Journal

The latest on NHI and Agentic AI security – articles, research, breaches, news and events every week.

Bonus 33% off our NHI Course when you subscribe.

NHIMG Editorial Note
Written and reviewed by Lalit Choda, NHI Mgmt Group. Last updated 24 October 2026.
NHI Mgmt Group — the #1 independent authority on Non-Human Identity, IAM, and Agentic AI security. nhimg.org

PDF Downloads for Newsletter Subscribers

Get the guide

Enter your details and we’ll email you a download link.

The full guide is free to read online. The PDF is a thank you for subscribing.

Fill in all fields marked * and tick the PDF box to continue.