Back to Blog
medium SEVERITY4 min read

Command Injection in Python Subprocess: A Security Fix Case Study

A medium-severity command injection vulnerability was discovered and fixed in a Python testing utility where unsanitized input could be passed to subprocess calls. This fix demonstrates the critical importance of input validation and safe subprocess handling to prevent attackers from executing arbitrary system commands.

O
By Orbis AppSec
Published May 20, 2026Reviewed June 3, 2026

Answer Summary

Command injection (CWE-78) occurs in Python when unsanitized user input is passed to subprocess functions like `subprocess.run()` or `subprocess.call()` with `shell=True`. The fix involves using parameterized argument lists instead of shell strings, setting `shell=False`, and implementing strict input validation with allowlists. This vulnerability allows attackers to execute arbitrary system commands by injecting shell metacharacters into user-controlled input.

Vulnerability at a Glance

cweCWE-78
fixUse shell=False with argument lists and implement input validation
riskArbitrary system command execution on the host
languagePython
root causeUnsanitized user input passed to subprocess with shell=True
vulnerabilityCommand Injection

Introduction

Command injection vulnerabilities remain one of the most dangerous security flaws in modern applications. When developers pass untrusted input to system commands without proper sanitization, they open the door for attackers to execute arbitrary code on the server. This week, we're examining a real-world fix for a command injection vulnerability (CWE-78) discovered in a Python testing utility.

Whether you're building CLI tools, automation scripts, or web applications that interact with the operating system, understanding this vulnerability class is essential for writing secure code.

The Vulnerability Explained

What Is Command Injection?

Command injection occurs when an application passes unsafe user-controlled data to a system shell. In Python, this commonly happens through functions like subprocess.run(), os.system(), or os.popen() when they're used improperly.

The vulnerability was found in selftest/run.py at line 30, where subprocess.run(cmd, **kwargs) was being called. The cmd parameter could potentially be constructed from:

  • Command-line arguments
  • Environment variables
  • Configuration files

How Could It Be Exploited?

When shell=True is used with subprocess calls and the command string includes unsanitized external input, an attacker can inject shell metacharacters to execute additional commands.

Consider this simplified vulnerable pattern:

# VULNERABLE CODE - DO NOT USE
import subprocess

def run_test(test_name):
    # test_name comes from user input
    cmd = f"python -m pytest {test_name}"
    subprocess.run(cmd, shell=True)  # DANGEROUS!

An attacker could provide input like:

test_file.py; rm -rf /important_data

This would result in the shell executing:

python -m pytest test_file.py; rm -rf /important_data

Real-World Impact

The consequences of command injection can be severe:

  • Data Exfiltration: Attackers can read sensitive files and send them to external servers
  • System Compromise: Full control over the affected server
  • Lateral Movement: Using the compromised system to attack other internal resources
  • Denial of Service: Crashing the system or consuming all resources
  • Ransomware Deployment: Installing malicious software

The Fix

The security fix addresses this vulnerability by sanitizing shell and subprocess calls in the run.py file. While the specific code diff wasn't provided, effective fixes for this vulnerability type typically involve one or more of the following approaches:

Approach 1: Avoid shell=True and Use List Arguments

# BEFORE (Vulnerable)
cmd = f"python -m pytest {test_name}"
subprocess.run(cmd, shell=True)

# AFTER (Secure)
cmd = ["python", "-m", "pytest", test_name]
subprocess.run(cmd, shell=False)  # shell=False is the default

By passing arguments as a list and avoiding shell=True, the subprocess module handles each argument safely without shell interpretation.

Approach 2: Input Validation and Sanitization

import re
import shlex

def sanitize_input(user_input):
    # Whitelist approach: only allow alphanumeric and safe characters
    if not re.match(r'^[\w\-./]+$', user_input):
        raise ValueError("Invalid characters in input")
    return user_input

def run_test(test_name):
    safe_name = sanitize_input(test_name)
    cmd = ["python", "-m", "pytest", safe_name]
    subprocess.run(cmd)

Approach 3: Use shlex.quote() When Shell Is Required

import shlex
import subprocess

def run_command(user_input):
    # If shell=True is absolutely necessary
    safe_input = shlex.quote(user_input)
    cmd = f"echo {safe_input}"
    subprocess.run(cmd, shell=True)

Prevention & Best Practices

1. Default to shell=False

Always use list-based command arguments with shell=False (the default). This is the single most effective prevention measure.

# Preferred approach
subprocess.run(["command", "arg1", "arg2"])

2. Validate and Sanitize All External Input

Never trust data from:
- User input (forms, APIs, CLI arguments)
- Environment variables
- Configuration files
- Database content

import re

SAFE_PATTERN = re.compile(r'^[a-zA-Z0-9_\-./]+$')

def validate_path(path):
    if not SAFE_PATTERN.match(path):
        raise ValueError(f"Invalid path: {path}")
    return path

3. Use Security Linters and Static Analysis

Tools that can detect command injection vulnerabilities:

  • Bandit: Python security linter (bandit -r your_project/)
  • Semgrep: Pattern-based static analysis
  • CodeQL: GitHub's semantic code analysis
  • SonarQube: Comprehensive code quality and security

4. Apply the Principle of Least Privilege

Run your applications with minimal system permissions. If command execution is compromised, the damage is limited to what that user account can access.

5. Reference Security Standards

  • CWE-78: Improper Neutralization of Special Elements used in an OS Command
  • OWASP Command Injection: Part of the OWASP Injection category
  • SANS Top 25: OS Command Injection is consistently ranked as a critical weakness

Security Checklist for Subprocess Calls

  • [ ] Using list arguments instead of string commands
  • [ ] shell=False (or not specified, as it's the default)
  • [ ] All external inputs validated against a whitelist
  • [ ] No string concatenation or f-strings for building commands
  • [ ] Security linter configured in CI/CD pipeline
  • [ ] Code review includes subprocess call inspection

Conclusion

Command injection vulnerabilities like the one fixed in selftest/run.py serve as important reminders that even utility scripts and testing tools require security attention. The fix demonstrates several key principles:

  1. Defense in depth: Multiple layers of protection are better than one
  2. Secure defaults: Use shell=False and list arguments by default
  3. Input validation: Never trust external data without validation
  4. Automated scanning: Use security tools in your CI/CD pipeline to catch issues early

As developers, we must treat every external input as potentially malicious. By following secure coding practices and leveraging automated security scanning, we can prevent command injection and similar vulnerabilities from reaching production.

Remember: security isn't a feature—it's a fundamental requirement of quality software.


Want to learn more about secure coding practices? Check out the OWASP Command Injection Prevention Cheat Sheet and CWE-78 for comprehensive guidance.

Frequently Asked Questions

What is command injection?

Command injection is a security vulnerability where an attacker can execute arbitrary operating system commands on a host machine by injecting malicious input into an application that passes user data to system shell commands without proper sanitization.

How do you prevent command injection in Python?

Prevent command injection by using `shell=False` with subprocess calls, passing arguments as a list instead of a string, implementing strict input validation with allowlists, and using the `shlex.quote()` function when shell execution is absolutely necessary.

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 alone enough to prevent command injection?

No, input validation should be combined with safe subprocess patterns. While validation helps, using `shell=False` with argument lists provides defense-in-depth by eliminating shell interpretation entirely, making injection impossible regardless of input content.

Can static analysis detect command injection?

Yes, static analysis tools like Semgrep, Bandit, and CodeQL can detect command injection patterns by identifying tainted data flows from user input sources to dangerous subprocess sinks, especially when `shell=True` is used.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #7

Related Articles

high

How missing Dependabot cooldown happens in GitHub Actions and how to fix it

A high-severity configuration vulnerability was discovered in a `.github/dependabot.yml` file that lacked a cooldown period for package updates. Without this safeguard, Dependabot could immediately propose updates to newly published package versions—including potentially malicious or unstable releases. The fix adds a simple `cooldown` block with a 7-day waiting period before any new package version is suggested.

high

How Server-Sent Events Injection via Unsanitized Newlines happens in Node.js h3 and how to fix it

A high-severity Server-Sent Events (SSE) injection vulnerability (CVE-2026-33128) was discovered in the h3 HTTP framework, where unsanitized newline characters in event stream fields could allow attackers to inject arbitrary SSE messages. The fix upgrades h3 from version 1.15.5 to 1.15.6 in the frontend's dependency tree, ensuring that newline characters are properly sanitized before being written to event streams.

high

How Memory Exhaustion via Large Comma-Separated Selector Lists happens in Python Soup Sieve and how to fix it

A high-severity memory exhaustion vulnerability (CVE-2026-49476) was discovered in Soup Sieve version 2.8.3, affecting Python applications that parse CSS selectors from user-controlled input. The vulnerability allows attackers to craft malicious selector lists that consume excessive memory, potentially causing denial of service. The fix involves upgrading to soupsieve 2.8.4, which implements proper resource limits on selector parsing.

high

How prototype pollution via `__proto__` key happens in Node.js defu and how to fix it

A high-severity prototype pollution vulnerability (CVE-2026-35209) was discovered in the `defu` package version 6.1.4, which allowed attackers to inject properties into JavaScript's `Object.prototype` via the `__proto__` key in defaults arguments. The fix upgrades `defu` to version 6.1.5 in the frontend's dependency tree, protecting downstream consumers like `c12` and `dotenv` configuration loaders from malicious property injection.

critical

How buffer overflow in memcpy() happens in Node.js N-API bindings and how to fix it

A critical buffer overflow vulnerability was discovered in the GetBufferAsVector() function in examples_nodejs/src/zupt_napi.cpp, where memcpy() copied data from JavaScript Uint8Array buffers without proper bounds validation. This vulnerability could allow attackers to trigger memory corruption by providing maliciously crafted input arrays to the native Node.js module, potentially leading to crashes or arbitrary code execution.

high

How memory exhaustion via large comma-separated selector lists happens in Python soupsieve and how to fix it

A high-severity memory exhaustion vulnerability (CVE-2026-49476) was discovered in soupsieve 2.8.3, a CSS selector library used by BeautifulSoup in Python. An attacker who could influence CSS selector input could craft large comma-separated selector lists to exhaust system memory, causing denial of service. The fix upgrades soupsieve from 2.8.3 to 2.8.4 in the backend's `uv.lock` dependency file.