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 Authentication Bypass happens in PyJWT and how to fix it

A critical authentication bypass vulnerability in PyJWT 2.12.1 allowed attackers to forge valid JSON Web Tokens, potentially bypassing application authentication mechanisms entirely. The vulnerability was fixed in PyJWT 2.13.0 through security improvements to token validation logic. This fix is essential for any application relying on JWT-based authentication.

high

How unsafe pickle deserialization happens in Keras/TensorFlow notebooks and how to fix it

A high-severity untrusted deserialization vulnerability was discovered in `TransferLearningTF.ipynb`, a transfer learning tutorial notebook that loads VGG16 model weights from the internet without verifying their integrity. Because Keras relies on Python's pickle-based serialization format under the hood, a tampered or substituted weights file could execute arbitrary code with the full privileges of the notebook user. The fix adds a SHA-256 checksum verification step immediately after the weight

critical

How Chromium launch-argument injection happens in Python Crawl4AI and how to fix it

A critical unauthenticated remote code execution vulnerability in Crawl4AI 0.8.9 allowed attackers to inject arbitrary Chromium launch arguments through the `browser_config.extra_args` parameter, potentially taking full control of the host process. The fix upgrades to Crawl4AI 0.9.0 and refactors the crawler initialization in `agent/tools/crawler.py` to use the new `BrowserConfig` and `CrawlerRunConfig` APIs, which enforce proper argument validation. This change eliminates the injection surface

critical

How integer overflow in buffer size calculation happens in C++ and how to fix it

A critical integer overflow vulnerability was discovered in OpenCV's HAL filter implementation where multiplying image dimensions without overflow protection could allocate dangerously undersized buffers. An attacker supplying crafted image dimensions (e.g., 65536×65536) could trigger heap corruption through out-of-bounds writes. The fix promotes the calculation to 64-bit arithmetic with a single cast.

critical

How buffer overflow via strcpy() happens in C zlib and how to fix it

A critical buffer overflow vulnerability was discovered in `general/libzlib/gzlib.c` where multiple `strcpy()` and `strcat()` calls operated without bounds checking. An attacker controlling file paths or error messages could overflow destination buffers, potentially achieving arbitrary code execution. The fix replaces these unsafe string operations with bounded `memcpy()` calls that respect pre-calculated buffer lengths.

critical

How hardcoded API key placeholders in documentation happen in Python and how to fix it

A high-severity security issue was discovered in the Context7 API documentation where a hardcoded API key placeholder (`CONTEXT7_API_KEY`) could be copied directly into production code. The fix replaced the static string with a proper environment variable reference using `os.environ`, preventing developers from accidentally deploying exposed credentials.