Dictionary Attack in Flask
How Dictionary Attack Manifests in Flask
A dictionary attack systematically attempts common passwords against an authentication endpoint, exploiting weak credential policies or missing rate controls. In Flask applications, this vulnerability typically arises from two intertwined oversights: inadequate password storage and absent request throttling.
Flask does not enforce password hashing or rate limiting by default. A vulnerable login route often stores passwords in plaintext or with weak hashes (e.g., MD5) and lacks any limit on failed attempts:
from flask import Flask, request
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
db = SQLAlchemy(app)
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True)
password = db.Column(db.String(120)) # Plaintext or weak hash!
@app.route('/login', methods=['POST'])
def login():
username = request.form['username']
password = request.form['password']
user = User.query.filter_by(username=username).first()
# VULNERABLE: Plaintext comparison
if user and user.password == password:
return {'status': 'success'}
return {'error': 'Invalid credentials'}, 401Without rate limiting, an attacker can script thousands of guesses per minute using tools like hydra or burp suite against /login. Even with hashed passwords, weak hashes (e.g., SHA-1) enable offline cracking. Flask's flexibility means developers must explicitly add protections; the framework itself provides no built-in brute-force defense.
Another Flask-specific pattern is exposing JWT issuance without throttling. If using flask-jwt-extended, a /auth endpoint that issues tokens on valid credentials but lacks rate limits becomes a prime target. Attackers can flood this endpoint with dictionary passwords, potentially locking out users or exhausting server resources.
Flask-Specific Detection
Detecting dictionary attack exposure in Flask requires examining both code and runtime behavior. Start by auditing all authentication routes (/login, /auth, /signin) for two signals: (1) absence of rate limiting, and (2) insecure password handling.
Manual Code Review: Search for routes accepting POST credentials. Check if Flask-Limiter or similar decorators are applied. Verify password storage uses strong algorithms (bcrypt, Argon2). Red flags include:
- No
@limiter.limitdecorator on login endpoints. - Direct password comparison (e.g.,
if user.password == password). - Use of
hashlib.md5orsha1without salt.
Dynamic Scanning with middleBrick: Submit your Flask API's base URL to middleBrick. Its Rate Limiting check automatically sends repeated requests to credential-accepting endpoints and observes if responses become throttled (HTTP 429) or if error messages change after multiple failures. The scanner also cross-references any OpenAPI/Swagger spec to locate /login paths and test them. A missing rate limit triggers a high-severity finding under the OWASP API Top 10 A2: Broken Authentication category. The report includes the exact endpoint tested and evidence of unthrottled requests.
# Example middleBrick CLI scan
$ middlebrick scan https://api.example.com
# Output snippet (JSON):
{
"score": 65,
"category_breakdown": {
"rate_limiting": {
"severity": "high",
"finding": "Login endpoint /v1/login accepts unlimited requests"
}
}
}Complementary tools like OWASP ZAP can manually brute-force the endpoint, but middleBrick automates this as part of its 12 parallel checks, providing a reproducible risk score (0–100) and remediation guidance specific to Flask's ecosystem.
Flask-Specific Remediation
Fix dictionary attack exposure in Flask by implementing two layers: rate limiting and strong password hashing. Use battle-tested extensions rather than rolling your own.
1. Enforce Rate Limiting with Flask-Limiter: Install flask-limiter and apply it to all authentication routes. Use a key function that locks by username or IP to prevent credential stuffing across accounts:
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
app = Flask(__name__)
limiter = Limiter(
app,
key_func=get_remote_address, # Or custom: lambda: request.form.get('username', get_remote_address())
default_limits=["100 per day", "10 per minute"]
)
@app.route('/login', methods=['POST'])
@limiter.limit("5 per 15 minutes", key_func=lambda: request.form.get('username', 'global'))
def login():
# ... authentication logic
return {'status': 'success'}This limits each username to 5 attempts per 15 minutes. Adjust limits based on your risk tolerance. The key_func ensures a single user's failures don't block others if using IP-based limits.
2. Hash Passwords with bcrypt: Never store plaintext. Use flask-bcrypt or passlib:
from flask_bcrypt import Bcrypt
bcrypt = Bcrypt(app)
# During user registration:
hashed_pw = bcrypt.generate_password_hash(password).decode('utf-8')
new_user = User(username=username, password=hashed_pw)
# During login:
if user and bcrypt.check_password_hash(user.password, password):
# Success
...3. Add Account Lockout (Optional but Recommended): Track failed attempts in your database and temporarily lock accounts after, say, 5 failures. This complements rate limiting but requires careful implementation to avoid denial-of-service via lockout.
After applying these fixes, rescan with middleBrick to confirm the Rate Limiting check passes and your security score improves. Remember: middleBrick only detects and reports; it does not modify your code. The remediation guidance it provides will point you to these Flask-specific patterns.