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?