Header Injection in Flask
How Header Injection Manifests in Flask
Header injection in Flask occurs when user-supplied data flows into HTTP response headers without proper validation or encoding. Flask's architecture, while providing many security features, can still be vulnerable to header injection through several specific patterns.
The most common Flask-specific manifestation happens through make_response() and response.headers manipulation. When developers dynamically construct headers using request data, they create injection opportunities:
from flask import Flask, request, make_response
app = Flask(__name__)
@app.route('/unsafe-header')
def unsafe_header():
user_id = request.args.get('id')
response = make_response('Hello')
response.headers['X-User-ID'] = user_id # Vulnerable if user_id contains newlines
return response
The vulnerability arises because HTTP headers are delimited by carriage return and line feed characters (\r\n). If an attacker supplies a value like 12345\r\nSet-Cookie: hacked=true, they can inject arbitrary headers into the response.
Flask's redirect() function presents another injection vector. The Location header is constructed from user input:
@app.route('/redirect-me')
def redirect_me():
url = request.args.get('url', '/') # Unsanitized user input
return redirect(url) # Vulnerable to header injection via url parameter
Flask's send_file() and send_from_directory() functions can also be exploited. These functions automatically set Content-Disposition headers based on filename parameters:
@app.route('/download')
def download():
filename = request.args.get('file', 'default.txt')
return send_from_directory('/uploads', filename) # Filename can contain newlines
Additionally, Flask's error handling can inadvertently expose header injection vulnerabilities. Custom error pages that include request data in response headers create attack surfaces:
@app.errorhandler(404)
def not_found(error):
user_agent = request.headers.get('User-Agent', '')
response = make_response(render_template('404.html'), 404)
response.headers['X-Debug-Info'] = user_agent # User-Agent can contain malicious data
return response
Flask's debug mode exacerbates these issues. When enabled, Flask includes stack traces in error responses, and if these traces incorporate request headers or parameters without sanitization, they can become injection vectors.
Flask-Specific Detection
Detecting header injection in Flask applications requires both manual code review and automated scanning. The key is identifying where user input flows into header construction.
Manual detection starts with searching for these Flask patterns:
# Patterns to search for in your Flask codebase
# 1. Direct header manipulation
response.headers['Header-Name'] = request.args.get('param')
# 2. make_response() usage
response = make_response(data)
response.headers.update(header_dict)
# 3. redirect() with dynamic URLs
return redirect(request.args.get('url'))
# 4. send_file() with dynamic parameters
return send_file(request.args.get('filename'))
# 5. Error handlers using request data
@app.errorhandler(400)
def handle_bad_request(e):
return jsonify(error=str(e)), 400, {'X-Debug': request.remote_addr}
middleBrick provides automated detection specifically tuned for Flask applications. The scanner identifies header injection vulnerabilities by:
- Analyzing your Flask routes and identifying dynamic header construction
- Testing for newline characters (
\r\n) in header values - Checking for unsafe URL construction in redirect functions
- Examining error handlers for request data exposure
- Scanning OpenAPI specs for header parameter definitions that could be exploited
To use middleBrick for Flask header injection detection:
# Install middleBrick CLI
npm install -g middlebrick
# Scan your Flask API endpoint
middlebrick scan https://your-flask-app.com/api/endpoint
# For CI/CD integration
middlebrick scan --threshold B --output json https://your-flask-app.com
middleBrick's Flask-specific checks include:
- Header Injection Test: Attempts to inject newline characters into response headers
- Redirect Validation: Tests for unsafe URL redirection and header manipulation
- Content-Disposition Analysis: Examines file download functionality for injection
- Debug Mode Detection: Identifies exposed debug endpoints that could leak sensitive information
The scanner provides detailed findings with severity levels and specific line numbers where vulnerabilities occur, making remediation straightforward.
Flask-Specific Remediation
Remediating header injection in Flask requires a defense-in-depth approach. Start with input validation and sanitization, then add architectural safeguards.
For direct header manipulation, always validate and sanitize user input:
from flask import Flask, request, make_response
import re
app = Flask(__name__)
# Whitelist approach for header values
ALLOWED_USER_IDS = re.compile(r'^[a-zA-Z0-9_-]+$')
def safe_header_value(value):
"""Remove newlines and validate against whitelist"""
if not value or '\r' in value or '\n' in value:
return None
if not ALLOWED_USER_IDS.match(value):
return None
return value
@app.route('/safe-header')
def safe_header():
user_id = request.args.get('id', 'anonymous')
safe_id = safe_header_value(user_id) or 'anonymous'
response = make_response('Hello')
response.headers['X-User-ID'] = safe_id
return response
For redirect functionality, implement URL validation and use Flask's built-in safety features:
from urllib.parse import urlparse, urljoin
from flask import request, redirect, url_for
# Whitelist of allowed redirect destinations
ALLOWED_HOSTS = {'yourdomain.com', 'api.yourservice.com'}
def is_safe_redirect_url(target_url):
"""Validate redirect URLs against a whitelist"""
if not target_url:
return False
parsed = urlparse(target_url)
if parsed.scheme not in ('http', 'https'):
return False
# Check if host is in allowed list or is relative
if parsed.netloc and parsed.netloc not in ALLOWED_HOSTS:
return False
return True
@app.route('/safe-redirect')
def safe_redirect():
target = request.args.get('url', url_for('index'))
if not is_safe_redirect_url(target):
return redirect(url_for('index')) # Default to safe destination
return redirect(target)
For file operations, sanitize filenames and use Flask's safe functions:
import os
from flask import send_from_directory
# Safe directory path
UPLOAD_DIRECTORY = '/var/www/uploads'
def safe_filename(filename):
"""Remove dangerous characters and path traversal"""
# Remove newlines and carriage returns
filename = filename.replace('\r', '').replace('\n', '')
# Remove path traversal attempts
filename = os.path.basename(filename)
# Whitelist allowed characters
if not re.match(r'^[\-_\.\w]+$', filename):
return 'default.txt'
return filename
@app.route('/safe-download')
def safe_download():
filename = request.args.get('file', 'default.txt')
safe_name = safe_filename(filename)
# Ensure file exists in allowed directory
full_path = os.path.join(UPLOAD_DIRECTORY, safe_name)
if not os.path.exists(full_path):
return 'File not found', 404
return send_from_directory(UPLOAD_DIRECTORY, safe_name)
Implement comprehensive error handling that doesn't expose request data:
from werkzeug.exceptions import HTTPException
@app.errorhandler(HTTPException)
def handle_exception(e):
"""Handle errors without exposing request data"""
response = e.get_response()
response.data = json.dumps({
'code': e.code,
'name': e.name,
'description': e.description
})
response.content_type = 'application/json'
return response
For production deployments, disable Flask's debug mode and use proper logging:
# In your application factory or main file
app = Flask(__name__)
app.config['DEBUG'] = False # Never enable in production
app.config['PROPAGATE_EXCEPTIONS'] = True
# Use structured logging instead of debug output
import logging
from logging.handlers import RotatingFileHandler
handler = RotatingFileHandler('app.log', maxBytes=10000, backupCount=1)
handler.setLevel(logging.INFO)
app.logger.addHandler(handler)
Finally, integrate middleBrick into your CI/CD pipeline to catch regressions:
# .github/workflows/security.yml
name: Security Scan
on: [push, pull_request]
jobs:
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install middleBrick
run: npm install -g middlebrick
- name: Scan Flask API
run: middlebrick scan https://staging.your-app.com --threshold B
- name: Fail on high severity issues
run: |
middlebrick scan https://staging.your-app.com --threshold B --exit-on-fail