Request Smuggling in Flask with Basic Auth
Request Smuggling in Flask with Basic Auth
Request smuggling occurs when an attacker can craft requests that are interpreted differently by separate components in the request path, such as a frontend proxy or load balancer and the application server. In Flask, this typically arises from inconsistent handling of message framing—especially the Content-Length header, Transfer-Encoding header, and request body parsing—when the application expects a clean, single interpretation of each HTTP message.
When Basic Authentication is used, the Authorization header is often added by an upstream proxy or API gateway before the request reaches Flask. If the proxy normalizes or splits headers differently than Flask, an attacker can smuggle a request by carefully arranging headers and body to create a secondary request that the proxy interprets as part of the first request, while Flask parses it as a separate request. Common patterns include:
- Sending a request with both Content-Length and Transfer-Encoding headers, where the proxy adheres to Transfer-Encoding and strips Content-Length, but Flask still parses based on Content-Length.
- Sending a request with a Content-Length that underreports the body size, causing Flask to treat part of the body as the next request.
- Using HTTP/1.1 features where a proxy handles chunked encoding or connection-level framing differently from Flask’s WSGI server, leading to request splitting when the Authorization header is present and interpreted inconsistently.
In a Flask app protected by Basic Auth, the vulnerability surface is shaped by how and where authentication is applied. If authentication is enforced at the proxy or gateway (via Basic Auth), and the proxy normalizes or terminates TLS, it may forward a sanitized request to Flask. However, if the proxy’s normalization diverges from Flask’s expectations—such as how it handles header case sensitivity, whitespace, or the sequence of headers—an attacker can craft a request where the smuggled path segment includes or omits the Authorization header. This can lead to the smuggled request being authenticated when it shouldn’t be, or being interpreted as an unauthenticated request to an endpoint that should require credentials, bypassing intended access controls.
For example, an attacker might send:
POST /api/resource HTTP/1.1
Host: example.com
Content-Type: application/json
Content-Length: 38
Transfer-Encoding: chunked
Authorization: Basic dXNlcjpwYXNz
38
{"action":"admin"}
0
GET /admin/secret HTTP/1.1
Host: example.com
Authorization: Basic dXNlcjpwYXNz
Depending on how the proxy and Flask parse the conflicting headers, Flask might process the second request with the same authentication context, potentially exposing admin functionality through the smuggled request. Because Flask’s development server is not designed for production proxy chaining, such edge cases are more likely to manifest when the app is behind a reverse proxy that handles Basic Auth and request routing.
Basic Auth-Specific Remediation in Flask
Remediation focuses on making header and body parsing predictable, avoiding reliance on potentially conflicting proxy behavior, and ensuring authentication is consistently applied at the application layer. Below are concrete fixes and patterns for Flask applications using HTTP Basic Authentication.
1. Use a robust WSGI server and avoid the built-in server in production
Never rely on Flask’s built-in server for production. Use a production-ready WSGI server such as Gunicorn or uWSGI, which handle message framing consistently and reduce the risk of interpretation differences between components.
2. Enforce strict header handling and reject ambiguous requests
Explicitly check for and reject requests that contain both Content-Length and Transfer-Encoding, or malformed chunked encodings. This prevents attackers from exploiting header parsing ambiguities.
from flask import Flask, request, abort
app = Flask(__name__)
@app.before_request
def reject_ambiguous_message_framing():
# Reject requests that provide both headers, which can be exploited for smuggling
if request.headers.get('Content-Length') and request.headers.get('Transfer-Encoding'):
abort(400, 'Ambiguous message framing not allowed')
3. Normalize and validate the Authorization header consistently
Ensure that the Authorization header is treated case-insensitively (per HTTP spec) and that its presence and format are validated before processing the request. Do not rely on upstream normalization.
from flask import Flask, request, abort
import base64
app = Flask(__name__)
def validate_basic_auth():
auth = request.headers.get('Authorization')
if not auth or not auth.startswith('Basic '):
abort(401, 'Missing or invalid authorization header')
try:
encoded = auth.split(' ')[1]
decoded = base64.b64decode(encoded).decode('utf-8')
if ':' not in decoded:
abort(401, 'Invalid credentials format')
username, password = decoded.split(':', 1)
# Replace with secure credential verification
if not (username == 'admin' and password == 'securepassword'):
abort(403, 'Invalid credentials')
except Exception:
abort(400, 'Malformed authorization token')
@app.before_request
def require_auth_for_protected_routes():
if request.path.startswith('/api/'):
validate_basic_auth()
@app.route('/api/resource', methods=['POST'])
def api_resource():
return {'status': 'ok'}
4. Avoid trusting client-provided Content-Length for body parsing
When using streaming or manual body parsing, validate that the body length matches Content-Length and that no extra data remains after reading the expected body. This prevents an attacker from smuggling requests by underreporting length.
import io
from flask import Flask, request, abort
app = Flask(__name__)
@app.route('/api/resource', methods=['POST'])
def api_resource():
content_length = int(request.headers.get('Content-Length', 0))
body = request.get_data(cache=False)
if len(body) != content_length:
abort(400, 'Content-Length mismatch')
# Process body safely
return {'received': body.decode('utf-8', errors='replace')}
5. Apply authentication at the application level, not only at the edge
Even if a proxy handles Basic Auth, enforce authentication within Flask for sensitive endpoints. This ensures that requests reaching your code are authenticated according to your application’s rules, regardless of proxy behavior.
# Example of route-level enforcement as a second layer
@app.route('/admin/dashboard')
def admin_dashboard():
validate_basic_auth()
return {'admin': True}
By combining strict message framing validation, consistent header processing, and application-level authentication, you reduce the risk of request smuggling in Flask applications that use Basic Authentication.