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

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

Mass Assignment Vulnerability: Why Your Rails Models Need attr_accessible

A medium-severity mass assignment vulnerability was identified in a Ruby on Rails model that lacked proper attribute whitelisting via `attr_accessible` or strong parameters. Without this protection, attackers can manipulate any model attribute through crafted HTTP requests, potentially escalating privileges or corrupting data. The fix enforces explicit attribute allowlisting, closing the door on unauthorized mass assignment exploitation.

critical

Shell Injection via os.system(): How a Single Line of Code Can Compromise Your System

A critical OS command injection vulnerability (CWE-78) was discovered and patched in `voice.py`, where user-controlled input was interpolated directly into a shell command string passed to `os.system()`. An attacker who could influence the `device` variable — through a config file, environment variable, or any external input — could execute arbitrary system commands with the full privileges of the running process. The fix replaces the dangerous `os.system()` calls with Python's `subprocess.run()

critical

Command Injection via os.system() in DeepSpeed's Data Analyzer: A Critical Fix

A critical command injection vulnerability was discovered in DeepSpeed's `data_analyzer.py`, where an `os.system()` call directly interpolated an unsanitized file path variable into a shell command string. An attacker who could influence dataset configuration or file paths could execute arbitrary shell commands on the host machine. The fix replaces the dangerous shell invocation with safe, Python-native file operations that never touch a shell interpreter.

high

CVE-2026-40073: How a BODY_SIZE_LIMIT Bypass in @sveltejs/adapter-node Put Your App at Risk

CVE-2026-40073 is a high-severity vulnerability in `@sveltejs/adapter-node` that allows attackers to bypass the `BODY_SIZE_LIMIT` configuration, potentially enabling denial-of-service attacks and resource exhaustion against SvelteKit applications. The vulnerability was silently present in versions prior to `@sveltejs/kit` 2.57.1, and has now been patched by upgrading the dependency across all affected project examples. If your application relies on body size limits to protect against oversized p

medium

From eval() to ast.literal_eval(): Closing a Code Injection Door in Slack Data Processing

A medium-severity vulnerability was discovered in a Slack data processing component where the use of Python's built-in `eval()` function to parse error message dictionaries could allow an attacker to inject and execute arbitrary code. The fix replaces `eval()` with the safer `ast.literal_eval()`, which safely evaluates only Python literals without executing arbitrary expressions. This change eliminates a critical attack surface that could have been exploited through crafted error messages return

critical

Critical Buffer Overflow in ELF Parser: How a Missing Bounds Check Almost Became a Heap Exploit

A critical out-of-bounds memory vulnerability was discovered and patched in `utils/symbol-rawelf.c`, where two separate `memcpy` calls lacked proper bounds validation when processing ELF binary files. Without these checks, a maliciously crafted ELF file could trigger an out-of-bounds read or heap overflow, potentially leading to remote code execution or memory corruption. This post breaks down how the vulnerability works, how it was fixed, and what every C developer should know about safe memory