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 orbisai0security
May 20, 2026
#security#python#command-injection#subprocess#CWE-78#secure-coding#devops

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.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #7

Related Articles

medium

Buffer Overflow in miniz.h: How a Missing Length Check Could Lead to Privilege Escalation

A medium-severity buffer overflow vulnerability was discovered and patched in the miniz.h file embedded within the KittyMemoryEx library, a memory manipulation tool used on Android and iOS platforms. The missing buffer-length check could have allowed attackers to exploit ZIP processing code to achieve arbitrary code execution with elevated privileges. This post breaks down how the vulnerability works, why it's dangerous in privileged contexts, and what developers can do to prevent similar issues

medium

Resource Exhaustion via Unchecked File Imports: How Missing Limits Create DoS Vulnerabilities

A medium-severity vulnerability in a file transfer receiver allowed attackers to exhaust server resources by sending maliciously crafted import files with no size limits, no JSON depth restrictions, and millions of entries loaded directly into memory. The fix introduces explicit input validation guards that reject unauthenticated or malformed requests before any disk or network operations begin. Understanding this class of vulnerability is essential for any developer building file ingestion pipe

medium

TOCTOU Symlink Attack Fixed: How Race Conditions Threaten Lock Files

A medium-severity TOCTOU (Time-of-Check to Time-of-Use) race condition vulnerability was discovered and fixed in a Rust application's lock file creation logic, where an attacker could exploit the window between a file existence check and its creation to redirect writes to an attacker-controlled path via a symlink. The fix applies the `O_NOFOLLOW` flag on Unix systems, ensuring the OS refuses to follow symlinks at the lock file path and fails loudly instead of silently writing to an attacker-cont