Password Spraying in Flask with Basic Auth
Password Spraying in Flask with Basic Auth — how this specific combination creates or exposes the vulnerability
Password spraying is an authentication-layer attack where a single common password is tried against many accounts. When Flask applications implement HTTP Basic Auth without protective controls, the attack surface is exposed directly through the Authorization header. Unlike credential stuffing that relies on breached username–password pairs, spraying uses one password (e.g., Password1) across many usernames, which can bypass simple account lockouts that only trigger on failed attempts per account.
In Flask, a naive Basic Auth implementation can inadvertently facilitate spraying if the endpoint does not enforce rate limiting or consistent response behavior. Consider this example that validates credentials but does not mitigate timing or request-rate abuse:
from flask import Flask, request, Response
def check_password(username, password):
# naive check: in real apps, use constant-time comparison and hashed storage
return username in {"alice", "bob"} and password == "Password1"
app = Flask(__name__)
@app.route("/api/protected")
def protected():
auth = request.authorization
if auth and check_password(auth.username, auth.password):
return Response("OK")
return Response("Unauthorized", 401, {"WWW-Authenticate": "Basic realm=\"Secure\""})
If an attacker sends many requests with different usernames and the same password, the application may reveal whether a username exists through subtle timing differences or by returning 401 uniformly. Without rate limiting, each request is effectively a free probe. Since Basic Auth transmits credentials in base64 (easily decoded) on every request, intercepted tokens remain valid until credentials change, compounding risk if spraying is successful.
The twelve parallel security checks in a middleBrick scan include Authentication, Rate Limiting, and Input Validation, which can identify whether your Flask endpoint is vulnerable to password spraying. For example, missing rate limiting allows unlimited attempts, and inconsistent authentication timing can leak account existence. middleBrick also supports OpenAPI/Swagger spec analysis (2.0, 3.0, 3.1) with full $ref resolution, cross-referencing spec definitions with runtime findings to highlight authentication weaknesses.
Because middleBrick scans the unauthenticated attack surface in 5–15 seconds, it can surface these issues without requiring credentials. The tool does not fix or block behavior; it reports findings with severity and remediation guidance, enabling you to harden the endpoint against spraying.
Basic Auth-Specific Remediation in Flask — concrete code fixes
Remediation focuses on making password spraying less effective and reducing the information an attacker can learn. Key measures include rate limiting, consistent timing responses, secure credential storage, and transport protection.
- Rate limiting: Apply per-username or per-source limits to throttle attempts. Use a reliable storage backend (e.g., Redis) to coordinate limits across workers.
- Constant-time comparison: Avoid early exits when comparing passwords to prevent timing-based enumeration.
- Secure password storage: Store passwords using a strong adaptive hash (e.g., Argon2 or bcrypt). Never compare plaintext passwords directly.
- Transport security: Enforce HTTPS so credentials are not exposed in transit; Basic Auth credentials are easily decoded if intercepted.
- Generic 401 responses: Return the same status and header regardless of whether the username is valid, to avoid account enumeration.
Here is a hardened Flask example that incorporates these practices. It uses werkzeug.security for constant-time comparison and bcrypt hashing, and integrates a simple per-username rate limiter using an in-memory dictionary (replace with a distributed store in production):
from flask import Flask, request, Response
import bcrypt
from werkzeug.security import safe_str_cmp
import time
app = Flask(__name__)
# In-memory rate limiter: {username: [timestamps]}
RATE_LIMIT_WINDOW = 60 # seconds
RATE_LIMIT_THRESHOLD = 5 # max attempts per window
attempts = {}
def is_rate_limited(username: str) -> bool:
now = time.time()
timestamps = attempts.get(username, [])
# purge old entries
recent = [t for t in timestamps if now - t < RATE_LIMIT_WINDOW]
attempts[username] = recent
return len(recent) >= RATE_LIMIT_THRESHOLD
def verify_password(stored_hash: bytes, password: str) -> bool:
# Constant-time check using safe_str_cmp on the hashed representation
return safe_str_cmp(stored_hash.decode(), bcrypt.hashpw(password.encode(), stored_hash).decode())
# Pre-seed users with bcrypt-hashed passwords (hashed once at startup)
USERS = {
"alice": bcrypt.hashpw(b"CorrectHorseBatteryStaple", bcrypt.gensalt()),
"bob": bcrypt.hashpw(b"AnotherStrongPassw0rd!", bcrypt.gensalt()),
}
@app.route("/api/protected")
def protected():
auth = request.authorization
if not auth or auth.username not in USERS:
# Always return 401 with the WWW-Authenticate header
return Response("Unauthorized", 401, {"WWW-Authenticate": 'Basic realm="Secure"'})
if is_rate_limited(auth.username):
# Rate limit exceeded: delay to prevent timing leaks and reject
time.sleep(1) # constant delay to obscure timing
return Response("Unauthorized", 401, {"WWW-Authenticate": 'Basic realm="Secure"'})
if verify_password(USERS[auth.username], auth.password):
return Response("OK")
# Record failed attempt
attempts.setdefault(auth.username, []).append(time.time())
return Response("Unauthorized", 401, {"WWW-Authenticate": 'Basic realm="Secure"'})
Deploying this pattern reduces the effectiveness of password spraying by limiting request rates, obscuring timing distinctions, and ensuring that credential leaks do not immediately compromise accounts. For broader API coverage, the middleBrick CLI can scan from terminal with middlebrick scan <url>, the GitHub Action can add API security checks to your CI/CD pipeline, and the MCP Server allows scanning APIs directly from your AI coding assistant.