422.42 Broken authentication
Prevent attackers from bypassing login systems and impersonating users by implementing strong authentication and secure session handling.
Overview
Broken authentication refers to flaws in login systems that allow attackers to gain unauthorised access. This includes failures to protect credentials, enforce login limits, or manage user sessions securely. Once exploited, broken authentication can lead to identity theft, data breaches, and complete system compromise.
Authentication systems must address two fundamental challenges: proving that users are who they claim to be (authentication) and maintaining that trust throughout their session (session management). When these systems fail, attackers can bypass security controls entirely, gaining the same access as legitimate users.
Understanding these vulnerabilities is crucial for securing the Flask applications you build, as authentication is often the primary defence protecting user data and system functionality.
Learning Targets
In this topic, students learn to:
Identify weaknesses in login and session management logic that create security vulnerabilities
Implement strong password handling practices, including secure hashing and storage
Apply session security measures to prevent unauthorised access
Prevent attackers from impersonating users or escalating privileges through authentication flaws
What Causes Broken Authentication?
Authentication systems fail for several common reasons that developers must understand and address:
Common Vulnerability Patterns
Credential-based attacks:
Credential stuffing: Using leaked usernames and passwords from other breaches to attempt login
Brute-force attacks: Systematically trying many password combinations to guess credentials
Weak password policies: Allowing easily guessable passwords
Session-based attacks:
Session hijacking: Stealing valid session tokens and reusing them for unauthorised access
Unexpired sessions: Sessions remaining valid indefinitely, even after logout
Predictable session IDs: Using sequential or easily guessable session identifiers
Technical vulnerabilities:
Weak password storage: Storing passwords in plaintext or using reversible encryption
Insufficient rate limiting: Allowing unlimited login attempts without restrictions
Secure Password Management
Strong password security forms the foundation of authentication systems. Passwords must be stored securely and validated properly to prevent compromise.
Vulnerable Password Storage
from flask import Flask, request, session
import sqlite3
app = Flask(__name__)
@app.route('/register', methods=['POST'])
def register_vulnerable():
username = request.form['username']
password = request.form['password']
# VULNERABLE: Storing passwords in plaintext
conn = sqlite3.connect('users.db')
cursor = conn.cursor()
cursor.execute("INSERT INTO users (username, password) VALUES (?, ?)",
(username, password))
conn.commit()
conn.close()
return "User registered"
@app.route('/login', methods=['POST'])
def login_vulnerable():
username = request.form['username']
password = request.form['password']
# VULNERABLE: Comparing plaintext passwords
conn = sqlite3.connect('users.db')
cursor = conn.cursor()
cursor.execute("SELECT * FROM users WHERE username = ? AND password = ?",
(username, password))
user = cursor.fetchone()
conn.close()
if user:
session['user_id'] = user[0]
return "Login successful"
return "Invalid credentials"
# Problems with this approach:
# 1. Anyone with database access can see all passwords
# 2. If database is breached, all passwords are immediately compromised
# 3. Support staff can see user passwords
# 4. No protection against credential reuse attacks
Secure Password Hashing
import hashlib
import secrets
from flask import Flask, request, session
import sqlite3
app = Flask(__name__)
app.secret_key = 'your-secret-key'
def hash_password(password):
"""
Create a secure hash of the password using SHA-256 with salt
Returns: salt and hash combined as a single string
"""
# Generate a random salt for this password
salt = secrets.token_hex(16) # 32-character hex string
# Combine password and salt, then hash
password_salt = password + salt
password_hash = hashlib.sha256(password_salt.encode()).hexdigest()
# Store salt and hash together (separated by $)
return f"{salt}${password_hash}"
def verify_password(password, stored_hash):
"""
Verify a password against the stored hash
Returns: True if password matches, False otherwise
"""
try:
# Split the stored hash to get salt and hash
salt, stored_password_hash = stored_hash.split('$')
# Hash the provided password with the same salt
password_salt = password + salt
password_hash = hashlib.sha256(password_salt.encode()).hexdigest()
# Compare hashes securely
return password_hash == stored_password_hash
except ValueError:
# Invalid hash format
return False
@app.route('/register', methods=['POST'])
def register_secure():
username = request.form['username']
password = request.form['password']
# SECURE: Hash password before storing
hashed_password = hash_password(password)
conn = sqlite3.connect('users.db')
cursor = conn.cursor()
try:
cursor.execute("INSERT INTO users (username, password_hash) VALUES (?, ?)",
(username, hashed_password))
conn.commit()
return "User registered successfully"
except sqlite3.IntegrityError:
return "Username already exists"
finally:
conn.close()
@app.route('/login', methods=['POST'])
def login_secure():
username = request.form['username']
password = request.form['password']
conn = sqlite3.connect('users.db')
cursor = conn.cursor()
cursor.execute("SELECT id, username, password_hash FROM users WHERE username = ?",
(username,))
user = cursor.fetchone()
conn.close()
# SECURE: Verify password against hash
if user and verify_password(password, user[2]):
session['user_id'] = user[0]
session['username'] = user[1]
return "Login successful"
return "Invalid credentials"
# Benefits of this approach:
# 1. Original passwords never stored in database
# 2. Each password has unique salt (prevents rainbow table attacks)
# 3. Even if database is breached, passwords remain protected
# 4. Impossible to reverse-engineer original passwords
Password Policy Enforcement
def validate_password(password):
"""
Validate password against security requirements
Returns: (is_valid, list_of_errors)
"""
errors = []
# Check minimum length
if len(password) < 8:
errors.append("Password must be at least 8 characters long")
# Check for required character types
if not any(c.islower() for c in password):
errors.append("Password must contain at least one lowercase letter")
if not any(c.isupper() for c in password):
errors.append("Password must contain at least one uppercase letter")
if not any(c.isdigit() for c in password):
errors.append("Password must contain at least one number")
# Check for special characters
special_chars = "!@#$%^&*(),.?\":{}|<>"
if not any(c in special_chars for c in password):
errors.append("Password must contain at least one special character")
# Check against common passwords
common_passwords = ['password', '123456', 'password123', 'admin', 'qwerty']
if password.lower() in common_passwords:
errors.append("Password is too common, please choose a different one")
return len(errors) == 0, errors
@app.route('/register', methods=['POST'])
def register_with_validation():
username = request.form['username']
password = request.form['password']
# Validate password strength
is_valid, errors = validate_password(password)
if not is_valid:
return f"Password validation failed: {', '.join(errors)}"
# Proceed with secure registration
hashed_password = hash_password(password)
# ... rest of registration code
Secure Session Management
Session management maintains the user authentication state across multiple requests. Poor session handling creates significant security vulnerabilities.
Vulnerable Session Handling
@app.route('/login', methods=['POST'])
def login_vulnerable_session():
username = request.form['username']
password = request.form['password']
if authenticate_user(username, password):
# VULNERABLE: Session never expires
session['user_id'] = get_user_id(username)
session['logged_in'] = True
# No session timeout set
return "Login successful"
return "Invalid credentials"
@app.route('/dashboard')
def dashboard_vulnerable():
# VULNERABLE: No session validation
if session.get('logged_in'):
return "Welcome to your dashboard"
return "Please log in"
# Problems:
# 1. Session never expires (even after browser closes)
# 2. No validation of session integrity
# 3. No protection against session hijacking
# 4. Sessions persist indefinitely on server
Secure Session Implementation
from datetime import datetime, timedelta
from flask import Flask, session, request
import secrets
app = Flask(__name__)
app.secret_key = secrets.token_hex(32) # Cryptographically secure secret
# Configure secure session cookies
app.config.update(
SESSION_COOKIE_SECURE=True, # Only send over HTTPS
SESSION_COOKIE_HTTPONLY=True, # Prevent JavaScript access
SESSION_COOKIE_SAMESITE='Strict', # Prevent CSRF attacks
PERMANENT_SESSION_LIFETIME=timedelta(minutes=30) # 30-minute timeout
)
@app.route('/login', methods=['POST'])
def login_secure_session():
username = request.form['username']
password = request.form['password']
if authenticate_user(username, password):
# SECURE: Create session with security metadata
session.permanent = True # Enable timeout
session['user_id'] = get_user_id(username)
session['username'] = username
session['login_time'] = datetime.now().isoformat()
session['last_activity'] = datetime.now().isoformat()
# Optional: Track IP address for security
session['ip_address'] = request.environ.get('REMOTE_ADDR')
return "Login successful"
return "Invalid credentials"
@app.route('/dashboard')
def dashboard_secure():
# SECURE: Validate session thoroughly
if not is_session_valid():
session.clear() # Clear invalid session
return "Session expired. Please log in again."
# Update last activity timestamp
session['last_activity'] = datetime.now().isoformat()
user_id = session['user_id']
username = session['username']
return f"Welcome to your dashboard, {username}"
def is_session_valid():
"""
Validate session security and timeout
Returns: True if session is valid, False otherwise
"""
# Check if user is logged in
if 'user_id' not in session:
return False
# Check session timeout
try:
last_activity = datetime.fromisoformat(session['last_activity'])
timeout_duration = timedelta(minutes=30)
if datetime.now() - last_activity > timeout_duration:
return False # Session expired
except (KeyError, ValueError):
return False # Invalid session data
# Optional: Check IP address consistency
current_ip = request.environ.get('REMOTE_ADDR')
session_ip = session.get('ip_address')
if session_ip and current_ip != session_ip:
# IP address changed - possible session hijacking
return False
return True
@app.route('/logout')
def logout():
# SECURE: Clear all session data
session.clear()
return "Logged out successfully"
@app.before_request
def check_session():
"""
Check session validity before each request
"""
# List of routes that don't require authentication
public_routes = ['login', 'register', 'static']
if request.endpoint not in public_routes:
if not is_session_valid():
session.clear()
return "Session expired. Please log in again.", 401
Rate Limiting and Brute Force Protection
Protecting against brute-force attacks requires implementing rate limiting and account lockout mechanisms.
from collections import defaultdict
from datetime import datetime, timedelta
# Simple in-memory rate limiting (use Redis in production)
login_attempts = defaultdict(list)
locked_accounts = {}
def is_account_locked(username):
"""
Check if account is currently locked due to failed attempts
Returns: (is_locked, seconds_remaining)
"""
if username in locked_accounts:
unlock_time = locked_accounts[username]
if datetime.now() < unlock_time:
remaining = (unlock_time - datetime.now()).seconds
return True, remaining
else:
# Lockout expired, remove it
del locked_accounts[username]
return False, 0
def record_login_attempt(username, success):
"""
Record login attempt and implement rate limiting
Returns: (is_now_locked, attempts_remaining)
"""
current_time = datetime.now()
# Clean old attempts (older than 1 hour)
cutoff_time = current_time - timedelta(hours=1)
login_attempts[username] = [
attempt_time for attempt_time in login_attempts[username]
if attempt_time > cutoff_time
]
if success:
# Successful login resets attempt counter
login_attempts[username] = []
if username in locked_accounts:
del locked_accounts[username]
return False, 5
# Record failed attempt
login_attempts[username].append(current_time)
# Check if account should be locked (5 attempts in 1 hour)
if len(login_attempts[username]) >= 5:
# Lock account for 15 minutes
locked_accounts[username] = current_time + timedelta(minutes=15)
return True, 0
remaining = 5 - len(login_attempts[username])
return False, remaining
@app.route('/login', methods=['POST'])
def login_with_rate_limiting():
username = request.form['username']
password = request.form['password']
# Check if account is locked
is_locked, time_remaining = is_account_locked(username)
if is_locked:
return f"Account locked. Try again in {time_remaining} seconds.", 429
# Attempt authentication
auth_success = authenticate_user(username, password)
# Record the attempt
now_locked, attempts_remaining = record_login_attempt(username, auth_success)
if auth_success:
# Set up secure session
session.permanent = True
session['user_id'] = get_user_id(username)
session['username'] = username
session['login_time'] = datetime.now().isoformat()
return "Login successful"
else:
if now_locked:
return "Too many failed attempts. Account locked for 15 minutes.", 429
else:
return f"Invalid credentials. {attempts_remaining} attempts remaining.", 401
def authenticate_user(username, password):
"""
Authenticate user credentials
Returns: True if valid, False otherwise
"""
conn = sqlite3.connect('users.db')
cursor = conn.cursor()
cursor.execute("SELECT password_hash FROM users WHERE username = ?", (username,))
result = cursor.fetchone()
conn.close()
if result:
stored_hash = result[0]
return verify_password(password, stored_hash)
return False
Common Authentication Vulnerabilities
SQL Injection in Login
# VULNERABLE: SQL injection risk
@app.route('/login_vulnerable', methods=['POST'])
def login_sql_injection():
username = request.form['username']
password = request.form['password']
# DANGEROUS: String concatenation in SQL
query = f"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'"
cursor.execute(query)
# Attack: username = admin'--
# Resulting query: SELECT * FROM users WHERE username = 'admin'-- AND password = '...'
# The -- comments out the password check!
# SECURE: Use parameterised queries
@app.route('/login_secure', methods=['POST'])
def login_no_sql_injection():
username = request.form['username']
password = request.form['password']
# SAFE: Parameterised query prevents injection
cursor.execute("SELECT * FROM users WHERE username = ? AND password_hash = ?",
(username, hash_password(password)))
Session Fixation Attack
# VULNERABLE: Session fixation
@app.route('/login_fixation', methods=['POST'])
def login_session_fixation():
if authenticate_user(username, password):
# VULNERABLE: Reusing existing session ID
session['user_id'] = get_user_id(username)
return "Login successful"
# SECURE: Regenerate session ID on login
@app.route('/login_no_fixation', methods=['POST'])
def login_prevent_fixation():
if authenticate_user(username, password):
# SECURE: Clear old session and create new one
old_session_data = dict(session)
session.clear()
# Flask automatically generates new session ID
session['user_id'] = get_user_id(username)
session['username'] = username
return "Login successful"
Code Interpretation Examples
# Example 1: Identify the vulnerability
@app.route('/login', methods=['POST'])
def weak_login():
username = request.form['username']
password = request.form['password']
# What's wrong with this code?
if username == "admin" and password == "password123":
session['logged_in'] = True
return "Welcome admin"
return "Access denied"
# Problems:
# 1. Hardcoded credentials
# 2. Weak password
# 3. No rate limiting
# 4. Credentials visible in source code
# Example 2: Identify session vulnerability
@app.route('/dashboard')
def user_dashboard():
user_id = session.get('user_id')
if user_id:
return f"Dashboard for user {user_id}"
return "Please log in"
# Problems:
# 1. No session timeout check
# 2. No session validation
# 3. Session could be expired but still accepted
# Example 3: Secure version
@app.route('/dashboard')
def secure_dashboard():
if not is_session_valid():
session.clear()
return "Session expired. Please log in."
user_id = session['user_id']
session['last_activity'] = datetime.now().isoformat()
return f"Dashboard for user {user_id}"
Real-World Examples in Flask Projects
# If your project has user registration
@app.route('/register', methods=['POST'])
def register():
# Validate input
username = request.form['username'].strip()
email = request.form['email'].strip()
password = request.form['password']
# Check password strength
is_valid, errors = validate_password(password)
if not is_valid:
return render_template('register.html', errors=errors)
# Hash password securely
password_hash = hash_password(password)
# Store in database
try:
cursor.execute(
"INSERT INTO users (username, email, password_hash) VALUES (?, ?, ?)",
(username, email, password_hash)
)
conn.commit()
return "Registration successful"
except sqlite3.IntegrityError:
return "Username or email already exists"
# Protected route example
@app.route('/profile')
def user_profile():
if not is_session_valid():
return redirect('/login')
user_id = session['user_id']
# Fetch and display user profile
return render_template('profile.html', user_id=user_id)
Summary
Broken authentication allows attackers to bypass login systems and access user accounts
Password security requires proper hashing with salts—never store plaintext passwords
Session management must include timeouts, validation, and secure cookie configuration
Rate limiting protects against brute-force attacks through attempt tracking and account lockouts
Flask provides tools for secure authentication through proper session handling and security headers
Common vulnerabilities include SQL injection, session fixation, and weak password policies
Strong authentication forms the foundation of application security. By implementing proper password handling, session management, and protective measures, you can create robust Flask applications that resist common authentication attacks while maintaining usability for legitimate users.
Last updated
Was this helpful?