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 buffer overflow in URL parsing happens in C++ HTTP client and how to fix it

A critical buffer overflow vulnerability in the HTTP client's URL parsing function allowed attackers to overflow a stack-allocated host buffer through specially crafted URLs with excessively long hostnames. The vulnerability enabled arbitrary code execution by overwriting the return address. The fix adds proper bounds validation before the memcpy() operation to ensure the hostname length never exceeds the destination buffer size.

critical

How integer overflow in _wopendir() happens in C Windows dirent and how to fix it

A critical integer overflow vulnerability in `include/compat/dirent_msvc.h` allowed an attacker-controlled directory path length to wrap the `sizeof(wchar_t) * n + 16` allocation calculation, resulting in a dangerously undersized heap buffer. Subsequent writes to that buffer caused a heap overflow, enabling potential memory corruption or code execution on Windows systems. The fix adds a pre-allocation bounds check and proper errno signaling to safely reject overflow-inducing inputs.

critical

How buffer overflow happens in C xxd utility and how to fix it

A critical buffer overflow vulnerability was discovered in the xxd utility's `xxdline()` function where `strcpy()` was used without bounds checking on file input. An attacker could craft a malicious hex dump file with oversized lines to trigger memory corruption. The fix replaces the unsafe `strcpy()` with `snprintf()` to enforce buffer size limits.

critical

How buffer overflow in memcpy() happens in C/C++ embedded firmware and how to fix it

A critical buffer overflow vulnerability was discovered in the ESP32-based micro-journal firmware where `memcpy()` calls used `strlen()` without bounds checking, allowing oversized USB descriptor strings to corrupt adjacent memory. The fix replaces unbounded `strlen()` with `strnlen()` calls that enforce the destination buffer sizes (8, 16, and 4 bytes respectively), preventing heap/stack corruption from malicious USB devices.

high

How Denial of Service via crafted URI templates happens in Ruby addressable and how to fix it

A high-severity Denial of Service vulnerability (CVE-2026-35611) was discovered in the Ruby `addressable` gem versions prior to 2.9.0, which could allow attackers to crash or hang applications by sending specially crafted URI templates. The fix upgrades the dependency from version 2.8.7 to 2.9.0 across the Gemfile, Gemfile.lock, and gemspec in a Fastlane project, eliminating the vulnerable code path entirely.

critical

How reflected XSS happens in Jinja2 template rendering and how to fix it

A reflected cross-site scripting (XSS) vulnerability was discovered in the similarity search HTML template where user input from the `query` form parameter was rendered directly into an HTML attribute without proper escaping. An attacker could inject malicious JavaScript by crafting a search query containing attribute-breaking payloads like `" onfocus="alert(document.cookie)" autofocus="`, which would execute in the victim's browser.