Back to Blog
critical SEVERITY5 min read

How command injection happens in Python subprocess and how to fix it

A critical command injection vulnerability was discovered in a CGI script that processed HTTP requests using `subprocess.check_output()` with `shell=True`. Attackers could inject arbitrary shell commands through URL parameters using metacharacters like semicolons, pipes, or backticks. The fix converts the command from a string to a list and sets `shell=False`, preventing shell interpretation of user input.

O
By Orbis AppSec
Published June 15, 2026Reviewed June 15, 2026

Answer Summary

This is a command injection vulnerability (CWE-78) in a Python CGI script where `subprocess.check_output()` was called with `shell=True`, allowing attackers to inject arbitrary commands through HTTP parameters. The fix converts the command string to a list and sets `shell=False`, which prevents the shell from interpreting metacharacters in user input as command separators or operators.

Vulnerability at a Glance

cweCWE-78
fixConvert command to list and use shell=False to prevent shell metacharacter interpretation
riskRemote code execution via HTTP request parameters
languagePython
root causeUsing shell=True with user-controlled command string in subprocess.check_output()
vulnerabilityOS Command Injection

Introduction

In the project/website/cmds/http_admin/cgi-bin/cgi_cmd_wrapper file, we discovered a critical command injection vulnerability that could have allowed remote attackers to execute arbitrary commands on the server. This CGI script processes HTTP requests and constructs shell commands from user-supplied parameters—a dangerous pattern that opened the door to complete system compromise.

The vulnerable code on line 22 used subprocess.check_output(cmd, shell=True) where the cmd variable was built directly from HTTP query parameters. This meant any attacker who could send an HTTP request to this endpoint could inject shell metacharacters and run any command they wanted on the server.

The Vulnerability Explained

The CGI script was designed to execute commands based on HTTP parameters. Here's the vulnerable code pattern:

form = cgi.FieldStorage()

cmd = "cgi-bin/" + form['c'].value

for item in cgi.parse().keys():
    if item != 'c':
        cmd += ' ' + form[item].value

result = subprocess.check_output(cmd, shell=True).decode("utf-8")

The critical flaw lies in how the command string is constructed and executed:

  1. String concatenation with user input: The cmd variable starts with a prefix but then appends raw user input from form['c'].value and other form parameters
  2. No input sanitization: User-supplied values are directly concatenated without any validation or escaping
  3. Shell execution enabled: The shell=True parameter tells Python to pass the command through a shell interpreter, which processes metacharacters

Attack Scenario

An attacker could craft a malicious HTTP request like:

GET /cgi-bin/cgi_cmd_wrapper?c=status;cat%20/etc/passwd HTTP/1.1

When this request is processed:
- The cmd variable becomes: cgi-bin/status;cat /etc/passwd
- The shell interprets the semicolon as a command separator
- First cgi-bin/status executes, then cat /etc/passwd executes
- The attacker receives the contents of /etc/passwd in the response

Other injection techniques work equally well:
- Pipe injection: ?c=status|id executes id and shows user context
- Backtick injection: ?c=status\whoami`executeswhoamiinline - **Command substitution**:?c=status$(rm -rf /)` could delete the entire filesystem

The impact is complete server compromise—attackers could read sensitive files, install backdoors, pivot to internal networks, or destroy data.

The Fix

The fix implements two crucial changes that eliminate the command injection vector:

Before (Vulnerable)

cmd = "cgi-bin/" + form['c'].value

for item in cgi.parse().keys():
    if item != 'c':
        cmd += ' ' + form[item].value

result = subprocess.check_output(cmd, shell=True).decode("utf-8")

After (Secure)

cmd = ["cgi-bin/" + form['c'].value]

for item in cgi.parse().keys():
    if item != 'c':
        cmd.append(form[item].value)

result = subprocess.check_output(cmd, shell=False).decode("utf-8")

Why This Fix Works

  1. Command as a list: By converting cmd from a string to a list, each argument is treated as a discrete element. The first element is the executable, and subsequent elements are its arguments.

  2. shell=False: With this setting, Python's subprocess module executes the command directly without invoking a shell interpreter. Shell metacharacters like ;, |, and backticks are treated as literal characters, not as operators.

  3. Argument isolation: When an attacker tries ?c=status;id, the entire string status;id becomes a single argument to cgi-bin/. The shell never sees it, so the semicolon has no special meaning—it's just part of a filename that likely doesn't exist.

The same attack request now results in:
- cmd = ["cgi-bin/status;id"]
- Python tries to execute a file literally named cgi-bin/status;id
- This file doesn't exist, so the command fails safely
- No injection occurs

Prevention & Best Practices

1. Always Use shell=False with Argument Lists

# DANGEROUS - Never do this with user input
subprocess.run(f"process {user_input}", shell=True)

# SAFE - Arguments are isolated
subprocess.run(["process", user_input], shell=False)

2. Validate and Whitelist Input

Even with shell=False, validate that input matches expected patterns:

import re

ALLOWED_COMMANDS = {'status', 'info', 'health'}

if form['c'].value not in ALLOWED_COMMANDS:
    raise ValueError("Invalid command")

3. Use shlex.quote() When Shell is Unavoidable

If you absolutely must use shell=True (which you almost never do), escape user input:

import shlex
safe_input = shlex.quote(user_input)

4. Apply Principle of Least Privilege

Run CGI scripts with minimal permissions. Use dedicated service accounts that can only access required resources.

5. Use Security Linters

Tools like Bandit can catch these patterns:

bandit -r your_project/

Bandit rule B602 specifically flags subprocess calls with shell=True.

Key Takeaways

  • Never use shell=True with any data derived from HTTP parameters—the cgi_cmd_wrapper script demonstrated how trivially this can be exploited
  • String concatenation for command building is inherently dangerous—the original code's cmd += ' ' + form[item].value pattern is a red flag
  • Converting commands to lists is a structural fix, not just a workaround—it changes how the operating system interprets the input
  • CGI scripts are particularly high-risk because they're directly network-accessible and designed to process untrusted input
  • Regression tests should verify injection resistance—the added test suite validates that semicolons, pipes, and backticks don't enable command execution

How Orbis AppSec Detected This

  • Source: HTTP request parameters via cgi.FieldStorage() and cgi.parse() in the CGI script
  • Sink: subprocess.check_output(cmd, shell=True) at line 22 of cgi_cmd_wrapper
  • Missing control: No input validation, sanitization, or safe subprocess invocation pattern
  • CWE: CWE-78 (Improper Neutralization of Special Elements used in an OS Command)
  • Fix: Converted command string to argument list and changed shell=True to shell=False

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.

Conclusion

This command injection vulnerability in cgi_cmd_wrapper represents one of the most dangerous patterns in web application security—taking user input and passing it directly to a shell. The fix demonstrates the correct approach: use subprocess with shell=False and pass arguments as a list, ensuring that shell metacharacters are never interpreted.

When reviewing code that executes system commands, always ask: "Can an attacker control any part of this command string?" If the answer is yes, restructure the code to use argument lists and disable shell interpretation. This structural change is more robust than any amount of input filtering.

References

Frequently Asked Questions

What is command injection?

Command injection is a vulnerability where an attacker can execute arbitrary operating system commands on the host by manipulating input that gets passed to a shell command, typically through metacharacters like semicolons, pipes, or backticks.

How do you prevent command injection in Python?

Use subprocess functions with `shell=False` and pass commands as a list of arguments rather than a string. This prevents the shell from interpreting metacharacters in user input as command operators.

What CWE is command injection?

Command injection is classified as CWE-78: Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

Is input validation enough to prevent command injection?

Input validation alone is not sufficient because it's difficult to anticipate all possible shell metacharacters and encoding variations. The safest approach is to avoid shell interpretation entirely by using `shell=False` with argument lists.

Can static analysis detect command injection?

Yes, static analysis tools can detect command injection patterns, particularly when `shell=True` is used with variables that may contain user input. Tools like Semgrep, Bandit, and commercial SAST solutions flag these patterns.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #4005

Related Articles

critical

How missing authorization enforcement happens in Node.js Express routers and how to fix it

A critical authorization bypass was discovered in lib/router.js where readOnly and noDelete configuration options were only enforced through UI controls, not server-side middleware. Any authenticated user could bypass these restrictions by sending direct HTTP requests to perform destructive operations like database deletion or document modification. The fix adds Express middleware that enforces these security modes at the server level, blocking POST, PUT, and DELETE requests when appropriate.

critical

How command injection happens in Java Runtime.exec() and how to fix it

A critical command injection vulnerability was discovered in `ProcessUtils.java` where process IDs were concatenated directly into shell command strings passed to `Runtime.getRuntime().exec(String)`. This allowed shell metacharacter injection that could execute arbitrary commands. The fix switches to the array-based `exec(String[])` overload, which bypasses shell interpretation entirely.

critical

How Algolia API key exposure happens in EJS templates and how to fix it

A critical vulnerability in `layout/_plugins/global/config.ejs` was exposing Algolia API credentials — including `appId` and `apiKey` — directly in rendered HTML, making them visible to any user who inspects the page source. The fix removes raw credential interpolation from the template and replaces unsafe string embedding with properly sanitized output using `JSON.stringify()`. This eliminates the risk of unauthorized Algolia API access by anyone who visits the page.

high

How unauthenticated endpoint exposure happens in Node.js Express and how to fix it

A high-severity vulnerability in the AgenticATODetectionService allowed unauthenticated users to access sensitive agent status data, trigger detection scans, and view security alerts. The fix adds authentication middleware to four critical API endpoints, ensuring only authorized users can access these sensitive operations.

critical

How unsigned auto-update code execution happens in Node.js Neutralinojs and how to fix it

A critical vulnerability in the WeekBox application's self-update mechanism allowed attackers to serve malicious binaries through man-in-the-middle attacks or repository compromise. The `app-updater.service.js` file downloaded and installed updates from GitHub Releases without enforcing cryptographic hash verification before proceeding with the update. The fix adds mandatory SHA-256 digest validation that halts the update process if a valid hash is not present in the release metadata.

critical

How command injection via shell metacharacter escaping happens in Node.js and how to fix it

A critical command injection vulnerability was discovered in the GameBanana provider module where the `quoteCommandArgument()` function only escaped double quotes, leaving shell metacharacters like `$()`, backticks, and other dangerous patterns exploitable. Attackers could craft malicious mod URLs on GameBanana containing shell commands that would execute when users viewed the content. The fix switches from double-quote to single-quote escaping, which prevents shell interpretation of metacharact