Command Injection in Flask
How Command Injection Manifests in Flask
Command injection in Flask applications typically occurs when user input flows directly into system calls without proper sanitization. Flask's flexibility with file uploads, configuration handling, and subprocess execution creates several attack vectors.
The most common pattern involves using Python's os.system(), subprocess.run(), or subprocess.Popen() with string concatenation. Consider this Flask route:
@app.route('/download', methods=['POST'])
def download_file():
filename = request.form.get('filename')
command = f'curl -o /tmp/{filename} http://example.com/{filename}'
os.system(command) # Vulnerable to command injection
return 'Download started'An attacker could submit file.txt; rm -rf / as the filename, causing the server to execute both the curl command and the destructive rm command.
File upload handlers present another vector. When Flask applications process uploaded files and pass their contents to system utilities:
@app.route('/process', methods=['POST'])
def process_image():
file = request.files['image']
file.save('/tmp/uploaded.png')
os.system(f'convert /tmp/uploaded.png -resize 100x100 /tmp/thumb.png')
return 'Processed'If the uploaded file contains malicious content that breaks out of the filename context, it could inject additional commands.
Flask's configuration system can also be exploited. When configuration values from environment variables or user input are used in system calls:
import os
from flask import Flask
app = Flask(__name__)
# Configuration from environment
app.config['DATA_DIR'] = os.getenv('DATA_DIR', '/data')
@app.route('/backup')
def backup():
cmd = f'zip -r /tmp/backup.zip {app.config["DATA_DIR"]}'
os.system(cmd)
return 'Backup created'If an attacker controls the DATA_DIR environment variable, they can inject arbitrary commands.
Template rendering can also lead to command injection when user input is embedded in system commands. Flask's Jinja2 templates are sandboxed, but developers sometimes bypass this for dynamic command generation:
@app.route('/execute')
def execute_command():
command = render_template_string('{{ request.args.cmd }}')
os.system(command)
return 'Executed'This pattern allows direct command injection through the URL parameter.
Flask-Specific Detection
Detecting command injection in Flask requires examining both the code patterns and runtime behavior. Static analysis tools can identify dangerous function calls, but Flask's dynamic nature means runtime scanning is crucial.
middleBrick's black-box scanning approach tests Flask endpoints by sending payloads designed to trigger command execution. For example, it might submit id; echo 'test' as a parameter and check if the response contains the expected output from the injected command.
The scanner examines Flask's typical response patterns. Flask applications often return JSON with specific structures, making it easier to identify when command output appears in responses. middleBrick looks for indicators like:
- Unexpected system command output in JSON responses
- HTTP status codes that suggest command execution (500 errors from malformed commands)
- Timing differences that indicate system call execution
- Changes in application state that suggest successful injection
For file upload endpoints, middleBrick tests with specially crafted files that contain command injection payloads. It monitors the application's behavior for signs of command execution, such as:
# Test payload for file upload injection
payload = b'; echo "INJECTED" > /tmp/test.txt;'
# middleBrick would upload this file and then check if /tmp/test.txt was createdmiddleBrick's OpenAPI analysis is particularly effective for Flask applications. Since Flask often uses Flask-RESTful, Flask-RESTX, or similar libraries that generate OpenAPI specs, middleBrick can:
- Map parameter locations (query, path, headers, body) to injection points
- Identify endpoints that accept file uploads or execute system operations
- Cross-reference parameter names with known dangerous patterns
The scanner also tests Flask's debug mode, which can expose additional attack surfaces. When Flask runs in debug mode, it provides an interactive debugger that could be exploited if accessible remotely.
For Flask applications using Celery or other task queues, middleBrick tests for command injection in task parameters, as these often flow through to system calls.
Flask-Specific Remediation
Remediating command injection in Flask requires eliminating unsafe system calls and using safer alternatives. The most effective approach is to avoid shell command execution entirely when possible.
For file operations that previously used system commands, use Python's native libraries:
# Vulnerable
os.system(f'convert /tmp/uploaded.png -resize 100x100 /tmp/thumb.png')
# Secure
from PIL import Image
with Image.open('/tmp/uploaded.png') as img:
img.thumbnail((100, 100))
img.save('/tmp/thumb.png')When system commands are unavoidable, use subprocess.run() with a list of arguments instead of a single string:
import subprocess
from flask import request
def safe_command_execution():
filename = request.form.get('filename', '').replace(';', '').replace('&', '')
# Use argument list, never shell=True
result = subprocess.run([
'curl',
'-o',
f'/tmp/{filename}',
f'http://example.com/{filename}'
], capture_output=True, text=True)
if result.returncode != 0:
return {'error': result.stderr}, 500
return {'message': 'Download completed'}Implement strict input validation and sanitization. For file names and paths:
import re
from flask import abort
def validate_filename(filename):
# Allow only alphanumeric, hyphens, underscores, and periods
if not re.match(r'^[\w,\-,\.]+$', filename):
abort(400, 'Invalid filename')
return filename
@app.route('/upload', methods=['POST'])
def upload_file():
file = request.files['file']
filename = validate_filename(file.filename)
file.save(f'/uploads/{filename}')
return 'File uploaded'For Flask applications that must execute shell commands, implement a command whitelist:
ALLOWED_COMMANDS = {
'ls': ['-l', '/tmp'],
'echo': ['Hello, World!']
}
def execute_whitelisted_command(cmd_name, args):
if cmd_name not in ALLOWED_COMMANDS:
raise ValueError('Command not allowed')
# Only allow specific arguments
allowed_args = ALLOWED_COMMANDS[cmd_name]
for arg in args:
if arg not in allowed_args:
raise ValueError('Argument not allowed')
result = subprocess.run([cmd_name] + args, capture_output=True, text=True)
return result.stdoutUse Flask's configuration system securely by validating configuration values:
import os
from flask import Flask
app = Flask(__name__)
# Validate configuration at startup
DATA_DIR = os.getenv('DATA_DIR', '/data')
if not os.path.isdir(DATA_DIR):
raise ValueError('Invalid DATA_DIR')
app.config['DATA_DIR'] = DATA_DIR
@app.route('/backup')
def backup():
# Use Python's zipfile instead of shell commands
import zipfile
with zipfile.ZipFile('/tmp/backup.zip', 'w') as backup_zip:
for root, _, files in os.walk(app.config['DATA_DIR']):
for file in files:
backup_zip.write(os.path.join(root, file))
return 'Backup created'Implement comprehensive logging to detect command injection attempts:
import logging
from flask import request
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
@app.before_request
def log_request():
logger.info(f"{request.method} {request.path} - {request.remote_addr}")
@app.route('/admin')
def admin_panel():
# This endpoint should never execute arbitrary commands
command = request.args.get('cmd', '')
if command:
logger.warning(f"Potential command injection attempt: {command}")
abort(400)
return 'Admin panel'Related CWEs: inputValidation
| CWE ID | Name | Severity |
|---|---|---|
| CWE-20 | Improper Input Validation | HIGH |
| CWE-22 | Path Traversal | HIGH |
| CWE-74 | Injection | CRITICAL |
| CWE-77 | Command Injection | CRITICAL |
| CWE-78 | OS Command Injection | CRITICAL |
| CWE-79 | Cross-site Scripting (XSS) | HIGH |
| CWE-89 | SQL Injection | CRITICAL |
| CWE-90 | LDAP Injection | HIGH |
| CWE-91 | XML Injection | HIGH |
| CWE-94 | Code Injection | CRITICAL |
Frequently Asked Questions
How can I test my Flask application for command injection vulnerabilities?
id; echo 'test' and checking for command output in responses. It also analyzes your OpenAPI spec if available, mapping parameter locations to injection points and testing file upload handlers with malicious content.What's the difference between using os.system() and subprocess.run() in Flask?
subprocess.run(['ls', '-l', '/tmp']) instead of os.system('ls -l /tmp'). This prevents shell metacharacters from being interpreted.