Command Injection in Flask with Basic Auth
Command Injection in Flask with Basic Auth — how this specific combination creates or exposes the vulnerability
Command Injection occurs when untrusted input is passed to a system shell or command interpreter without proper validation or escaping. In a Flask application that uses HTTP Basic Authentication, the combination of credential handling, dynamic parameter usage, and subprocess invocation can create conditions where an authenticated (or unauthenticated, depending on implementation) attacker is able to inject and execute arbitrary commands.
Flask does not provide built-in protection against command injection; it relies on the developer to sanitize and validate any external input. When endpoints that accept user-controlled data—such as filenames, IP addresses, or query parameters—are used in functions like os.system, subprocess.run, or os.popen, the risk emerges. If Basic Auth is implemented naively, for example using a hardcoded check or a simple token comparison, an attacker may manipulate authentication headers or parameters to influence command construction. Moreover, if error messages or logs inadvertently reveal command execution details, they can aid further exploitation.
The vulnerability is not inherent to Basic Auth itself, but to how input flows through the application after authentication. For instance, an endpoint that accepts a hostname and passes it to a ping command can become an injection vector if the input is not properly constrained. Attack patterns such as shell metacharacters (;, &, |, `, $( )) enable command chaining or redirection, potentially bypassing intended logic and executing privileged operations.
Basic Auth-Specific Remediation in Flask — concrete code fixes
Securing Flask endpoints that use Basic Auth and external commands requires strict input validation, avoiding shell interpretation, and safe credential handling. Below are concrete remediation steps and code examples.
1. Avoid the shell for external commands
Use subprocess.run with a list of arguments and shell=False (the default) to prevent the shell from interpreting metacharacters. Never build command strings by concatenating user input.
import subprocess
from flask import Flask, request, jsonify
app = Flask(__name__)
# Safe: arguments passed as a list, no shell involved
def safe_ping(hostname):
result = subprocess.run(['ping', '-c', '1', hostname], capture_output=True, text=True)
return result.stdout, result.stderr
@app.route('/ping', methods=['GET'])
def ping():
host = request.args.get('host', '')
# Basic input validation: allow only alphanumeric, dash, and dot
if not all(c.isalnum() or c in ('-', '.') for c in host):
return jsonify({'error': 'Invalid host parameter'}), 400
stdout, stderr = safe_ping(host)
return jsonify({'output': stdout, 'error': stderr})
2. Secure Basic Authentication implementation
Implement Basic Auth without leaking credentials and with constant-time comparison where applicable. Use environment variables for secrets and avoid hardcoded passwords.
import os
import base64
import secrets
from flask import Flask, request, Response, jsonify
app = Flask(__name__)
# Example: retrieve credentials from environment
VALID_USER = os.environ.get('API_USER', 'admin')
VALID_PASS = os.environ.get('API_PASS')
def check_auth(username, password):
# Use secrets.compare_digest to avoid timing attacks
return secrets.compare_digest(username, VALID_USER) and secrets.compare_digest(password, VALID_PASS)
def authenticate():
return Response(
'Authentication required', 401,
{'WWW-Authenticate': 'Basic realm="API"'}
)
@app.before_request
def require_auth():
if request.endpoint in ['login', 'static']:
return
auth = request.authorization
if not auth or not check_auth(auth.username, auth.password):
return authenticate()
@app.route('/login', methods=['GET'])
def login():
return jsonify({'status': 'authenticated'})
3. Input validation and allowlisting
Define strict allowlists for inputs that affect command execution. For hostnames, restrict to DNS-safe characters; for numeric IDs, enforce integer parsing and range checks.
from flask import request, jsonify
import re
def validate_hostname(host):
# Allow lowercase letters, digits, hyphens, and dots; no shell metacharacters
pattern = re.compile(r'^[a-z0-9]([a-z0-9\-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9\-]*[a-z0-9])?)*$')
return bool(pattern.fullmatch(host))
@app.route('/check', methods=['GET'])
def check():
host = request.args.get('host', '')
if not validate_hostname(host):
return jsonify({'error': 'Invalid host'}), 400
# Proceed with safe command usage
return jsonify({'host': host})
4. Principle of least privilege and environment hardening
Run the Flask application with minimal OS permissions. Ensure environment variables containing credentials are not exposed in logs or error pages. Disable debug mode in production to prevent code execution via introspection.
# Run the app with a non-root user and restricted environment
# Example launch command (not part of app code):
# export FLASK_ENV=production
# export API_USER=svcuser
# export API_PASS=$(openssl rand -base64 16)
# flask run --host=0.0.0.0 --port=5000
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 |