Security Misconfiguration in Flask with Basic Auth
Security Misconfiguration in Flask with Basic Auth — how this specific combination creates or exposes the vulnerability
Security misconfiguration in a Flask app using HTTP Basic Authentication commonly arises from a combination of default or weak settings and improper handling of credentials. When Basic Auth is enabled without enforcing HTTPS, credentials are transmitted in base64 encoding that is trivial to decode. A base64 string is not encryption; it offers no confidentiality. If TLS is missing or misconfigured (for example, allowing both HTTP and HTTPS or using self-signed certificates without strict verification), an attacker on the network can intercept credentials via passive sniffing or active downgrade attacks.
Another misconfiguration is verbose error messages that disclose stack traces or internal paths, which can help an attacker refine injection or enumeration. Flask’s default debug mode, if left enabled in production, exposes a debugger that can lead to remote code execution and reveals configuration details useful for further exploitation. Hardcoded credentials or storing passwords in plain text in source code or configuration files also constitute misconfiguration; this makes it easy for an attacker who gains filesystem access to recover credentials immediately.
Insecure defaults extend to how authentication is implemented. For example, using flask.request.authorization naively without validating and sanitizing the provided username and password can lead to injection issues when those values are later used in system commands or logs. Additionally, missing protections like account lockout or rate limiting on the login endpoint enables credential brute-forcing. Because Basic Auth sends credentials with every request, the attack surface is larger: any leaked token (for instance, via logs or browser history) can be reused until it expires. Misconfigured CORS can also expose authentication endpoints to unauthorized origins, further widening the risk. These issues are magnified when Basic Auth is used without additional controls, such as short token lifetimes or multi-factor mechanisms.
Basic Auth-Specific Remediation in Flask — concrete code fixes
To remediate misconfiguration, enforce HTTPS for all traffic and never transmit Basic Auth credentials over HTTP. Use strong hashing for stored passwords (e.g., bcrypt) rather than plain text, and avoid hardcoding credentials in source code. Apply robust error handling to prevent information leakage, and add rate limiting to mitigate brute-force attempts. Below are concrete, secure code examples for a Flask application using HTTP Basic Auth.
Enforce HTTPS and require secure transmission
Ensure your deployment terminates TLS and that Flask is configured to reject plain HTTP. In development, you can use an SSL context with self-signed certificates for testing only.
from flask import Flask, request, Response
import ssl
app = Flask(__name__)
# Enforce HTTPS in production by using a proper reverse proxy (e.g., Nginx, Traefik)
# and setting PREFERRED_URL_SCHEME to 'https' if behind a proxy that sets headers.
# This snippet is for illustration; do not rely on Flask alone for TLS enforcement.
@app.before_request
def enforce_https():
if not request.is_secure:
return "Use HTTPS", 403
Secure Basic Auth implementation with hashed credentials and error handling
Store credentials as salted hashes. Verify the provided password against the hash rather than comparing plaintext secrets.
from flask import Flask, request, Response
import bcrypt
app = Flask(__name__)
# Example stored hash for user 'alice' with password 'CorrectHorseBatteryStaple'
# Generated with: bcrypt.hashpw(b'CorrectHorseBatteryStaple', bcrypt.gensalt())
USERS = {
"alice": b'$2b$12$KIXgUuKp8y6qJzZ9YVvE2uK7l8m9n0o1p2q3r4s5t6u7v8w9x0y1z2'
}
def verify_password(username, password):
if username not in USERS:
# Use a dummy hash to prevent timing attacks
bcrypt.hashpw(b'dummy', bcrypt.gensalt())
return False
return bcrypt.checkpw(password.encode('utf-8'), USERS[username])
@app.route('/')
def index():
auth = request.authorization
if not auth or not verify_password(auth.username, auth.password):
return Response(
'Could not verify your access level.',
401,
{'WWW-Authenticate': 'Basic realm="Login Required"'}
)
return f'Authenticated as {auth.username}'
Add error handling and avoid information leakage
Ensure errors do not expose stack traces or paths. Use generic messages and log securely for further analysis.
import logging
from flask import Flask, request, Response
app = Flask(__name__)
app.config['PROPAGATE_EXCEPTIONS'] = False
# Configure structured logging for auth events (do not log passwords)
logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s')
@app.errorhandler(500)
def handle_500(e):
app.logger.error('Server error', exc_info=True)
return Response('Internal server error', status=500)
@app.route('/')
def index():
auth = request.authorization
if not auth:
return Response('Auth required', 401, {'WWW-Authenticate': 'Basic realm="Login Required"'})
# Authentication logic here
return 'OK'
Apply rate limiting to the authentication endpoint
Use a lightweight in-memory store or integrate with a shared store for production. This example uses a simple dictionary for demonstration; prefer Flask-Limiter or a Redis-backed store in production.
from flask import Flask, request, Response
from time import time
app = Flask(__name__)
# Simple rate limiter per IP for demonstration; use Redis or Flask-Limiter in production.
attempts = {}
def is_rate_limited(ip):
now = time()
window = 60 # seconds
limit = 5 # max attempts per window
attempts.setdefault(ip, [])
attempts[ip] = [t for t in attempts[ip] if now - t < window]
if len(attempts[ip]) >= limit:
return True
attempts[ip].append(now)
return False
@app.route('/')
def index():
if is_rate_limited(request.remote_addr):
return Response('Too many requests', 429)
auth = request.authorization
if not auth or not verify_password(auth.username, auth.password):
return Response('Bad credentials', 401)
return 'OK'