Back

A2:2017 - Broken Authentication - Examples

How to Read These Examples

Each example shows a vulnerable implementation and the secure version that fixes it, in the same language, so the specific change is easy to spot. The four languages cover the same underlying lessons—correct hashing, session regeneration, cookie flags, rate limiting, generic errors, and MFA verified before session elevation—so read across them to see the pattern repeat.

PHP: Password Storage & Login

Vulnerable

<?php
// login.php  — multiple classic A2 flaws
$user = $db->query("SELECT * FROM users WHERE name = '$username'")->fetch();

// FLAW 1: password stored/compared as unsalted MD5
if ($user && $user['password'] === md5($password)) {
    // FLAW 2: no session regeneration -> session fixation
    $_SESSION['uid'] = $user['id'];
    header("Location: /dashboard.php");
} else {
    // FLAW 3: message reveals whether the username exists (enumeration)
    if (!$user) {
        echo "No such user";
    } else {
        echo "Wrong password";
    }
    // FLAW 4: no rate limiting anywhere -> brute force / stuffing
}
?>

Secure

<?php
// login.php — hardened
// (Assume $pdo is a PDO connection and a rate-limiter service is available.)

if (!$rateLimiter->allow($username, $_SERVER['REMOTE_ADDR'])) {
    http_response_code(429);
    exit("Too many attempts. Please try again later.");
}

// Parameterized query (also closes SQL injection)
$stmt = $pdo->prepare("SELECT id, password_hash FROM users WHERE name = ?");
$stmt->execute([$username]);
$user = $stmt->fetch();

// Constant-time-ish: always run password_verify, even for unknown users,
// against a dummy hash so timing does not reveal account existence.
$hash = $user['password_hash'] ?? '$2y$12$'.str_repeat('.', 53);
$valid = password_verify($password, $hash);

if ($user && $valid) {
    session_regenerate_id(true);           // FIX: kill session fixation
    $_SESSION['uid'] = $user['id'];
    // Upgrade the hash transparently if the cost factor has changed:
    if (password_needs_rehash($hash, PASSWORD_BCRYPT, ['cost' => 12])) {
        $new = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]);
        $pdo->prepare("UPDATE users SET password_hash = ? WHERE id = ?")
            ->execute([$new, $user['id']]);
    }
    $rateLimiter->reset($username);
    header("Location: /dashboard.php");
} else {
    $rateLimiter->recordFailure($username, $_SERVER['REMOTE_ADDR']);
    echo "Invalid username or password.";  // FIX: generic message
}

// Registration stores a proper hash:
//   $hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]);
?>

Python / Flask: Login, Rate Limit & Session

Vulnerable

from flask import Flask, request, session, redirect
import hashlib

app = Flask(__name__)
app.secret_key = "hardcoded-secret"        # FLAW: secret in source

@app.route("/login", methods=["POST"])
def login():
    u = request.form["username"]
    p = request.form["password"]
    row = db.execute("SELECT id, pw FROM users WHERE name = ?", (u,)).fetchone()

    # FLAW: fast, unsalted SHA-256 for passwords; == is not constant-time
    if row and row["pw"] == hashlib.sha256(p.encode()).hexdigest():
        session["uid"] = row["id"]         # FLAW: no session regeneration
        return redirect("/home")
    return "Login failed: user not found" if not row else "Wrong password"
    # FLAW: enumeration + no rate limiting + insecure cookie defaults

Secure

import os, time
from flask import Flask, request, session, redirect, abort
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError

app = Flask(__name__)
app.secret_key = os.environ["SESSION_SECRET"]        # FIX: from environment
app.config.update(
    SESSION_COOKIE_SECURE=True,      # FIX: HTTPS-only cookie
    SESSION_COOKIE_HTTPONLY=True,    # FIX: not readable by JS
    SESSION_COOKIE_SAMESITE="Lax",   # FIX: limit cross-site sending
    PERMANENT_SESSION_LIFETIME=1800, # FIX: absolute timeout (30 min)
)
ph = PasswordHasher()
DUMMY = ph.hash("throwaway")                          # for uniform timing

@app.route("/login", methods=["POST"])
def login():
    u, p = request.form["username"], request.form["password"]
    if not rate_limiter.allow(u, request.remote_addr):
        abort(429)

    row = db.execute("SELECT id, pw_hash FROM users WHERE name = ?",
                     (u,)).fetchone()
    target_hash = row["pw_hash"] if row else DUMMY    # always verify something
    ok = False
    try:
        ph.verify(target_hash, p)
        ok = row is not None
    except VerifyMismatchError:
        ok = False

    if ok:
        session.clear()                               # FIX: drop pre-auth state
        session["uid"] = row["id"]                    # new session id issued
        session["auth_at"] = time.time()
        rate_limiter.reset(u)
        if ph.check_needs_rehash(target_hash):
            db.execute("UPDATE users SET pw_hash = ? WHERE id = ?",
                       (ph.hash(p), row["id"]))
        return redirect("/home")

    rate_limiter.record_failure(u, request.remote_addr)
    return "Invalid username or password.", 401       # FIX: generic message

Node.js / Express: Session & Cookie Handling

Vulnerable

const express = require('express');
const session = require('express-session');
const app = express();

app.use(session({
  secret: 'keyboard cat',           // FLAW: weak, hardcoded secret
  resave: true,
  saveUninitialized: true,
  // FLAW: cookie defaults — not Secure, not HttpOnly-enforced, no SameSite
}));

app.post('/login', (req, res) => {
  const { username, password } = req.body;
  const user = users.find(u => u.name === username);
  // FLAW: plaintext password comparison
  if (user && user.password === password) {
    req.session.uid = user.id;      // FLAW: no session regeneration
    return res.redirect('/home');
  }
  // FLAW: enumeration message + no throttling
  res.send(user ? 'Wrong password' : 'Unknown user');
});

// FLAW: logout only clears the cookie client-side
app.get('/logout', (req, res) => res.clearCookie('connect.sid').redirect('/'));

Secure

const express = require('express');
const session = require('express-session');
const bcrypt = require('bcrypt');
const rateLimit = require('express-rate-limit');
const app = express();
app.set('trust proxy', 1);

app.use(session({
  secret: process.env.SESSION_SECRET,      // FIX: strong secret from env
  name: 'sid',
  resave: false,
  saveUninitialized: false,
  cookie: { httpOnly: true, secure: true, sameSite: 'lax', maxAge: 1800000 },
}));

const loginLimiter = rateLimit({ windowMs: 15*60*1000, max: 20 }); // FIX

const DUMMY_HASH = bcrypt.hashSync('throwaway', 12);   // uniform timing

app.post('/login', loginLimiter, async (req, res) => {
  const { username, password } = req.body;
  const user = await db.findUserByName(username);
  const hash = user ? user.passwordHash : DUMMY_HASH;  // always compare
  const ok = (await bcrypt.compare(password, hash)) && !!user;

  if (!ok) {
    return res.status(401).send('Invalid username or password.'); // FIX
  }
  // FIX: regenerate session id on login (prevents fixation)
  req.session.regenerate(err => {
    if (err) return res.sendStatus(500);
    req.session.uid = user.id;
    req.session.authAt = Date.now();
    res.redirect('/home');
  });
});

// FIX: destroy the session server-side on logout
app.post('/logout', (req, res) => {
  req.session.destroy(() => res.clearCookie('sid').redirect('/'));
});

Java / Spring: Password Hashing & MFA

Vulnerable

// Plain servlet-style login — several A2 flaws
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
        throws IOException {
    String user = req.getParameter("username");
    String pass = req.getParameter("password");

    User u = userDao.findByName(user);
    // FLAW: SHA-1 hex compare, non-constant-time, no salt
    String hashed = DigestUtils.sha1Hex(pass);
    if (u != null && u.getHash().equals(hashed)) {
        // FLAW: reuse the existing session -> fixation
        req.getSession().setAttribute("uid", u.getId());
        resp.sendRedirect("/home");
    } else {
        // FLAW: enumeration + no lockout + no MFA
        resp.getWriter().println(u == null ? "No such user" : "Bad password");
    }
}

Secure

// Spring Security config: strong hashing, session fixation protection
@Bean
public PasswordEncoder passwordEncoder() {
    // FIX: Argon2 (or BCryptPasswordEncoder(12)) — salted, slow, tunable
    return new Argon2PasswordEncoder(16, 32, 1, 1 << 14, 2);
}

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
      .authorizeHttpRequests(a -> a.anyRequest().authenticated())
      .formLogin(form -> form.loginPage("/login")
          // FIX: generic failure — no username enumeration
          .failureUrl("/login?error"))
      .sessionManagement(s -> s
          // FIX: new session id on authentication (anti-fixation)
          .sessionFixation(sf -> sf.newSession())
          .maximumSessions(1))                       // limit concurrent sessions
      .logout(l -> l.invalidateHttpSession(true)      // FIX: server-side logout
          .deleteCookies("JSESSIONID"));
    return http.build();
}

// FIX: verify the second factor BEFORE completing authentication
public boolean verifyTotp(User user, String code) {
    // Throttle attempts separately; codes are short-lived (RFC 6238)
    return totpVerifier.isValid(user.getTotpSecret(), code);
}

// Secure cookie flags (application.properties):
//   server.servlet.session.cookie.secure=true
//   server.servlet.session.cookie.http-only=true
//   server.servlet.session.cookie.same-site=lax
//   server.servlet.session.timeout=30m

Summary of Fixes

Vulnerable patternSecure replacement
Plaintext / MD5 / SHA-1 / SHA-256 passwordArgon2id / bcrypt (salted, slow, auto-rehash)
== / .equals() on secretsLibrary verify / constant-time compare
Reuse session ID after loginRegenerate session ID on login (anti-fixation)
"No such user" vs "wrong password"Single generic message + uniform timing
Unlimited login attemptsRate limit per account + per IP, backoff
Default / weak cookie flagsSecure; HttpOnly; SameSite + HSTS
Logout clears cookie onlyDestroy session server-side; revoke on pw change
Hardcoded secret in sourceSecret from environment / secret store
No second factorMFA verified before session elevation

Next Steps

Put it into practice: Open the lab at ./lab/broken-authentication/ (docker-compose up --build), locate the vulnerable login and session code, and apply the secure versions shown here.