HIGH command injectionflask

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 created

middleBrick'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.stdout

Use 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 IDNameSeverity
CWE-20Improper Input Validation HIGH
CWE-22Path Traversal HIGH
CWE-74Injection CRITICAL
CWE-77Command Injection CRITICAL
CWE-78OS Command Injection CRITICAL
CWE-79Cross-site Scripting (XSS) HIGH
CWE-89SQL Injection CRITICAL
CWE-90LDAP Injection HIGH
CWE-91XML Injection HIGH
CWE-94Code Injection CRITICAL

Frequently Asked Questions

How can I test my Flask application for command injection vulnerabilities?
Use middleBrick's 10-second self-service scan by submitting your Flask API endpoint URL. The scanner tests for command injection by sending payloads like 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?
os.system() is dangerous because it always invokes a shell, making it vulnerable to command injection. subprocess.run() is safer when used correctly—pass arguments as a list instead of a single string, and never set shell=True. For example, use subprocess.run(['ls', '-l', '/tmp']) instead of os.system('ls -l /tmp'). This prevents shell metacharacters from being interpreted.