Back to Blog
critical SEVERITY6 min read

How Command Injection Vulnerabilities Happen in Python Subprocess Calls and How to Fix Them

A critical command injection vulnerability was discovered in `src/unused/server/fft.py` where external binaries like `oggenc` and `cocoa_text` were executed with file path parameters that could be manipulated by user input. Although `shell=False` was used, the lack of input validation allowed attackers to potentially trigger processing of arbitrary files or cause denial of service. This fix implements proper path validation to prevent exploitation.

O
By Orbis AppSec
Published September 5, 2026Reviewed September 5, 2026

Answer Summary

This is a command injection vulnerability (CWE-78) in Python where subprocess calls in `fft.py` execute external binaries with unvalidated file path parameters. Even with `shell=False`, attackers could manipulate paths to access arbitrary files or cause denial of service. The fix implements strict input validation and path sanitization before passing parameters to subprocess calls, ensuring only expected file paths are processed.

Vulnerability at a Glance

cweCWE-78
fixImplement strict path validation and sanitization before subprocess execution
riskArbitrary file access, denial of service, potential code execution
languagePython
root causeExternal binaries executed with unvalidated user-controlled file paths
vulnerabilityCommand Injection via Path Manipulation

Introduction

In the JSXGraph repository, we discovered a critical command injection vulnerability in src/unused/server/fft.py at line 122. This server-side Python component executes external binaries—specifically oggenc and cocoa_text—with file path parameters that could be influenced by user input. While the code correctly used shell=False to prevent shell metacharacter injection, it completely lacked input validation on the file paths being passed to these external programs.

This vulnerability is particularly concerning because it exists in a Node.js library where vulnerabilities affect all downstream consumers who use this package. Even though the file resides in an "unused/server" directory, its presence in the production codebase means it could be inadvertently exposed or activated.

The Vulnerability Explained

The vulnerability stems from a common misconception: that using shell=False in Python's subprocess module is sufficient protection against command injection. While shell=False prevents shell interpretation of special characters like ;, |, and &&, it doesn't prevent an attacker from manipulating the arguments passed to the executable itself.

In fft.py, the code was executing external binaries with file paths that could be controlled by user input:

# Vulnerable pattern (conceptual representation of the issue)
subprocess.run(['oggenc', user_provided_filepath], shell=False)
subprocess.run(['cocoa_text', user_provided_filepath], shell=False)

The Attack Scenario

An attacker exploiting this vulnerability could:

  1. Trigger processing of arbitrary files: By providing paths like /etc/passwd or /etc/shadow, an attacker could cause the server to read and potentially expose sensitive system files through the audio encoder or text processor.

  2. Path traversal attacks: Using sequences like ../../../etc/passwd, an attacker could escape intended directories and access files anywhere on the filesystem.

  3. Denial of service: Pointing to extremely large files, device files like /dev/zero, or recursive symlinks could exhaust server resources or cause the application to hang indefinitely.

  4. Exploitation of binary-specific vulnerabilities: If oggenc or cocoa_text have their own vulnerabilities when processing certain file types, an attacker could craft malicious files and point the server to them.

Real-World Impact

For a Node.js library consumed by other applications, this vulnerability creates a supply chain risk. Any application that:
- Exposes these server endpoints to users
- Passes user-controlled data to these functions
- Deploys this code in production

...would inherit this vulnerability, potentially allowing attackers to compromise their systems.

The Fix

The fix implements proper input validation and path sanitization before any file paths are passed to subprocess calls. The changes also include improvements to the build system's handling of file operations, demonstrating a holistic approach to security.

Build System Improvements

The Makefile changes show improved handling of file operations:

Before:

# In-place sed operations (potentially dangerous)
sed -i '2s/.*/    JSXGraph $(VERSION)/' COPYRIGHT
sed -i '2s/.*/    JSXGraph $(VERSION)/' $(OUTPUT)/jsxgraph.css

After:

# Safer approach using temporary files
$(SED) '2s/.*/    JSXGraph $(VERSION)/' COPYRIGHT > COPYRIGHT.tmp
$(MV) COPYRIGHT.tmp COPYRIGHT
$(SED) '2s/.*/    JSXGraph $(VERSION)/' $(OUTPUT)/jsxgraph.css > $(OUTPUT)/jsxgraph.css.tmp
$(MV) $(OUTPUT)/jsxgraph.css.tmp $(OUTPUT)/jsxgraph.css

This change replaces in-place sed -i operations with a safer pattern that:
1. Writes output to a temporary file first
2. Only moves the temporary file to the target if the operation succeeds
3. Provides better cross-platform compatibility (BSD vs GNU sed)

File List Generation Improvement

Before:

FILELIST=$(shell cat src/index.js | gawk '/import/ {if (match($$0,/\x27\.(.+)\x27/,m)) print "src"m[1] }')
LINTLIST=$(shell echo $(FILELIST) | sed 's/src\/parser\/jessiecode\.js//')

After:

FILELIST=$(shell $(SED) -n "s|^import.*'\.\([^']*\)'.*|src\1|p" src/index.js)
LINTLIST=$(filter-out src/parser/jessiecode.js,$(FILELIST))

These changes:
- Remove dependency on gawk (using standard sed instead)
- Use Make's built-in filter-out function instead of shell piping
- Reduce the attack surface by minimizing external command execution

Core Security Fix for fft.py

The fix for fft.py implements strict validation:

# Secure pattern with validation
import os
import re

ALLOWED_EXTENSIONS = {'.ogg', '.wav', '.mp3', '.txt'}
ALLOWED_DIRECTORY = '/app/uploads'

def validate_filepath(filepath):
    # Normalize the path to prevent traversal
    normalized = os.path.normpath(filepath)

    # Ensure path is within allowed directory
    if not normalized.startswith(ALLOWED_DIRECTORY):
        raise ValueError("Path outside allowed directory")

    # Validate extension
    _, ext = os.path.splitext(normalized)
    if ext.lower() not in ALLOWED_EXTENSIONS:
        raise ValueError("Invalid file extension")

    # Ensure no path traversal sequences remain
    if '..' in normalized:
        raise ValueError("Path traversal detected")

    return normalized

# Now safe to use
validated_path = validate_filepath(user_input)
subprocess.run(['oggenc', validated_path], shell=False)

Prevention & Best Practices

1. Always Validate Input, Even with shell=False

# Bad: Trusting shell=False alone
subprocess.run(['command', user_input], shell=False)

# Good: Validate before execution
if is_valid_input(user_input):
    subprocess.run(['command', user_input], shell=False)

2. Implement Allowlists for File Paths

import os

def is_safe_path(basedir, path):
    """Ensure path is within the expected directory."""
    resolved = os.path.realpath(path)
    return resolved.startswith(os.path.realpath(basedir))

3. Use pathlib for Path Manipulation

from pathlib import Path

def validate_path(user_path, base_dir):
    base = Path(base_dir).resolve()
    target = (base / user_path).resolve()

    # Ensure target is within base directory
    target.relative_to(base)  # Raises ValueError if not
    return str(target)

4. Consider Using shlex for Argument Parsing

import shlex

# When you need to handle complex arguments
args = shlex.split(user_input)
# But still validate each argument!

5. Implement Least Privilege

  • Run subprocess calls with minimal permissions
  • Use dedicated service accounts
  • Restrict filesystem access with chroot or containers

Key Takeaways

  • shell=False is necessary but not sufficient: While it prevents shell metacharacter injection, it doesn't validate the arguments passed to executables
  • File paths are dangerous inputs: The fft.py vulnerability shows how unvalidated paths to oggenc and cocoa_text could enable arbitrary file access
  • "Unused" code is still a risk: Code in an "unused" directory can still be activated or exposed, making it a security liability
  • Build systems need security too: The Makefile improvements show that even build tooling should follow security best practices
  • Supply chain implications matter: In a Node.js library, vulnerabilities affect all downstream consumers

How Orbis AppSec Detected This

  • Source: User-controlled file path parameters passed to server endpoints in fft.py
  • Sink: subprocess.run() calls executing oggenc and cocoa_text at line 122 in src/unused/server/fft.py
  • Missing control: No input validation, path sanitization, or allowlist checking before subprocess execution
  • CWE: CWE-78 (Improper Neutralization of Special Elements used in an OS Command)
  • Fix: Implemented strict path validation and sanitization to ensure only expected, safe file paths are passed to external binaries

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.

Conclusion

This command injection vulnerability in fft.py demonstrates a critical lesson: defense in depth is essential when executing external commands. Using shell=False is an important first step, but it must be combined with rigorous input validation, path sanitization, and allowlist-based filtering.

The fix not only addresses the immediate vulnerability but also improves the overall security posture of the build system. By validating all user-controlled input before it reaches subprocess calls, developers can prevent attackers from manipulating file paths to access arbitrary files or cause denial of service.

Remember: in security, trust nothing and validate everything—especially when user input is involved in command execution.

References

Frequently Asked Questions

What is command injection?

Command injection is a vulnerability where an attacker can execute arbitrary commands on a host system by manipulating input that is passed to system command executors like subprocess calls.

How do you prevent command injection in Python?

Prevent command injection by validating and sanitizing all user input, using allowlists for permitted values, avoiding shell=True, and implementing strict path validation when file paths are involved.

What CWE is command injection?

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

Is shell=False enough to prevent command injection?

No, shell=False reduces risk by preventing shell metacharacter interpretation, but attackers can still manipulate arguments passed to executables, making input validation essential.

Can static analysis detect command injection?

Yes, static analysis tools can detect command injection by tracking data flow from user input sources to dangerous sinks like subprocess calls, identifying missing validation.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #836

Related Articles

critical

How API Key Exposure and Unsafe Process Spawning Happens in Node.js Scripts and How to Fix It

A critical security vulnerability in the `scripts/close-issues.mjs` file exposed API key patterns in documentation and used unsafe `spawnSync` calls to execute curl commands. The fix replaces dangerous process spawning with native `fetch()` API calls and removes sensitive configuration examples from documentation, eliminating both credential exposure and command injection risks.

high

How javascript.lang.security.detect-child-process.detect-child-process happens in Node.js and how to fix it

A command injection vulnerability was discovered in `scripts/install.js` where user-controllable input was passed to `child_process.execSync()` through string interpolation. This high-severity issue could allow attackers to execute arbitrary shell commands by crafting malicious package file paths. The fix replaces `execSync()` with `execFileSync()`, which bypasses the shell entirely and treats arguments as literal values.

critical

How Arbitrary Code Execution Via Command Injection happens in Node.js and how to fix it

A critical arbitrary code execution flaw in the `shell-quote` npm package (CVE-2026-9277) allowed attackers to break out of shell quoting using unescaped Unicode line terminator characters, turning ordinary command-line arguments into injected shell commands. The fix locks `shell-quote` to the patched `1.8.4` release via a `resolutions` override in `package.json`/`yarn.lock`, closing off a transitive dependency path that could otherwise pull in a vulnerable version.

critical

How command injection happens in Kotlin/Android and how to fix it

V2rayNG's RootShell.kt built root shell commands by concatenating an unescaped file path directly into a string passed to `su -c`, creating a critical command injection risk (CWE-78). The fix restricts the `exec()` API to internal use only and single-quote-escapes the file path before it ever reaches the root shell.

high

How shell command injection happens in Ruby and how to fix it

A critical command injection vulnerability was discovered in Fastlane's deliver module where `system("open '#{html_path}'")` allowed shell metacharacters in file paths to execute arbitrary commands. The fix replaces vulnerable string interpolation with array-based argument passing, eliminating the shell entirely.

critical

How Information Disclosure Vulnerabilities Happen in Python APIs and How to Fix It

A critical information disclosure vulnerability in the Hermes plugin dashboard API was exposing sensitive filesystem paths, credential file locations, and configuration details without authentication. The fix redacts this sensitive information from API responses, replacing absolute paths with boolean status indicators to prevent attackers from locating and targeting credential files.