Cross Site Request Forgery in Flask with Basic Auth
Cross Site Request Forgery in Flask with Basic Auth — how this specific combination creates or exposes the vulnerability
Cross Site Request Forgery (CSRF) in Flask when using HTTP Basic Authentication involves a specific interaction between browser credential handling and lack of anti-CSRF tokens. Basic Auth credentials are typically sent in the Authorization header on every request to a protected endpoint. If a user is logged into a site that uses Basic Auth and visits a malicious site, the browser may automatically include those credentials for the target origin, enabling unauthorized actions without user consent.
Consider a Flask route that performs a sensitive state-changing operation like changing an email address or initiating a fund transfer. If this route accepts GET requests or does not validate the origin of the request, an attacker can craft an image or script tag that points to that route. Because the browser attaches Basic Auth headers automatically, the server may process the request as legitimate, assuming the user intended the action. This becomes more pronounced when endpoints do not require a CSRF token and rely solely on the presence of credentials in headers.
Flask does not provide built-in CSRF protection. When using Basic Auth, developers might assume that requiring credentials is sufficient, but authentication and authorization are not substitutes for CSRF mitigation. Attack vectors often involve social engineering to get the victim to visit a malicious page while already authenticated. Tools can fingerprint protected routes by probing for 200 versus 401 responses and then use forged requests to test for missing origin or referer checks.
Using the middleBrick scanner, such misconfigurations are surfaced in findings like missing CSRF protections and improper validation of the Origin header. The scanner tests unauthenticated attack surfaces and can detect endpoints that accept state-changing methods without verifying request provenance. Even when Basic Auth is used, the absence of synchronizer token patterns or custom headers like X-Requested-With can lead to a high risk score for CSRF-related items.
Real-world examples align with the OWASP API Security Top 10 and common weaknesses such as CWE-352. For instance, a route like /api/change-email that accepts POST but does not validate the request origin can be exploited by an attacker who knows the Basic Auth credentials are sent automatically. This is especially relevant in internal or microservice environments where endpoints are assumed to be safe from CSRF because they are not browser-facing, but they remain vulnerable when accessed via browsers or embedded clients.
Basic Auth-Specific Remediation in Flask — concrete code fixes
To mitigate CSRF in Flask with Basic Auth, combine proper authentication patterns with anti-CSRF techniques. Since Basic Auth credentials are sent automatically by the browser to the target origin, you must ensure that state-changing requests are not executable via simple GET requests and that each request includes a verifiable token or header that an attacker cannot forge.
One effective approach is to require custom headers or tokens for any non-GET request and to validate the Origin or Referer header where appropriate. Below is a concrete Flask example that uses HTTP Basic Auth and requires a custom X-CSRFToken header for state-changing operations.
from flask import Flask, request, abort, jsonify
from functools import wraps
import secrets
app = Flask(__name__)
# In-memory store for tokens per session (in practice, use a secure session store)
csrf_tokens = {}
def get_auth_user(auth_header):
# Simple Basic Auth decoding for demonstration
import base64
if not auth_header or not auth_header.startswith('Basic '):
return None
encoded = auth_header.split(' ')[1]
try:
decoded = base64.b64decode(encoded).decode('utf-8')
username, password = decoded.split(':', 1)
return {'username': username}
except Exception:
return None
def csrf_protect(f):
@wraps(f)
def decorated(*args, **kwargs):
if request.method in ['POST', 'PUT', 'DELETE', 'PATCH']:
token = request.headers.get('X-CSRFToken')
if not token:
abort(403, description='Missing CSRF token')
# In a real app, validate token against session or origin
return f(*args, **kwargs)
return decorated
@app.route('/api/change-email', methods=['POST'])
def change_email():
auth = request.headers.get('Authorization')
user = get_auth_user(auth)
if not user:
abort(401, description='Invalid credentials')
# Validate CSRF token for state-changing actions
token = request.headers.get('X-CSRFToken')
if not token or token != csrf_tokens.get(user['username']):
abort(403, description='Invalid CSRF token')
# Proceed with email change logic
return jsonify({'status': 'success'})
@app.route('/api/get-token', methods=['GET'])
def get_csrf_token():
auth = request.headers.get('Authorization')
user = get_auth_user(auth)
if not user:
abort(401, description='Invalid credentials')
token = secrets.token_hex(16)
csrf_tokens[user['username']] = token
return jsonify({'csrf_token': token})
if __name__ == '__main__':
app.run(debug=False)
In this pattern, the client first retrieves a CSRF token via a GET endpoint that also validates Basic Auth. Subsequent state-changing requests must include this token in a custom header. This prevents attackers from forging requests from external sites because they cannot read the token due to the same-origin policy. For APIs consumed by browsers, also check the Origin header to reject cross-origin requests that do not match your domain.
Using the middleBrick CLI, you can verify that these mitigations are effective by scanning your endpoints and confirming that protected routes require both authentication and CSRF tokens. The scanner reports findings mapped to frameworks like OWASP API Top 10, helping you prioritize fixes.