Skip to main content

All weeks · Worksheet · Overview · Attack notes

Week 6 · Lecture slides

Week 6

Contents23 sections

Authentication, Sessions & Access Control

Software Security · Nutthakorn Chalaemwongwan


Today

  • Authn vs authz
  • Session management & JWT pitfalls
  • OAuth2 / OIDC at a glance
  • IDOR & broken access control
  • 🎮 Game: IDOR Treasure Hunt + JWT Forgery

Recap — Week 5

  • XSS steals sessions → today we manage them
  • Client-side trust is limited — enforce on the server

Authn vs Authz

  • Authentication — who are you? (login, MFA)
  • Authorization — what are you allowed to do?
  • Most breaches today are authz failures
  • Maps to A01 Broken Access Control, A07 Authentication Failures

Sessions

  • Server issues a session token after login
  • Classic pattern: stored in a cookie (HttpOnly, Secure, SameSite)
  • Risks: fixation, predictable IDs, no expiry, no logout invalidation
  • Today's lab is 100% JWT bearer-token — no cookies at all. Of these four risks, only "no expiry" is what you'll actually exercise (the fix adds a 15-min exp claim); fixation/predictable-ID/logout-invalidation are cookie-session concepts, general knowledge, not tested here

JWT pitfalls

  • alg: none accepted → forge any token
  • Weak/guessable HMAC secret → re-sign
  • Not checking exp / aud / signature at all
  • Sensitive data in payload (it's only base64!)
Editing a JWT: base64url is encoding, not sealing (Week 6) — open full size

The fix: verify the signature — one check, every endpoint

# vulnerable: 'none' allowed + weak secret -> any forged token passes
if alg == "none": jwt.decode(token, options={"verify_signature": False})
SECRET = "secret"

# fixed: pin the algorithm, strong secret, require exp + aud
jwt.decode(token, SECRET, algorithms=["HS256"],
           audience=AUD, options={"require": ["exp", "aud"]})
  • base64url is encoding, not a seal — only the signature check stops forgery
  • Pin algorithms=["HS256"] (never "none") + a strong random secret from env
  • Fix it in one place (current_user) → every endpoint is covered at once

JWT forgery in one picture

A JWT is header.payload.signature in base64url: the header and payload are readable and editable by anyone, only the signature is a seal. Two forgeries: alg:none sets an empty signature and a vulnerable server skips the check and accepts sub:admin; a weak secret lets the attacker re-sign sub:admin so the signature verifies. The fix pins algorithms to HS256, uses a strong secret, and requires exp and aud, so both forged tokens return 401 — and because the check lives in current_user it covers every endpoint at once.


OAuth2 / OIDC (high level)

  • Delegated access via tokens — don't share passwords
  • OIDC adds identity (ID token) on top of OAuth2
  • Common bugs: open redirect, missing state, token leakage

IDOR & broken access control

GET /api/orders/1   → your order
GET /api/orders/2   → someone else's  😱
  • Object reference with no ownership check
  • Vertical (become admin) vs horizontal (other users)

The fix: check ownership — a valid token is not permission

# vulnerable: authenticated, but the result is thrown away -> no Gate 2
def get_order(oid):
    current_user()                 # WHO you are...
    return jsonify(ORDERS[oid])    # ...but never 'may you see THIS?'

# fixed: deny-by-default ownership check on every object access
    if order["owner"] != user: return 403
  • Authentication answers who; authorization answers may you touch THIS object
  • alice's real, valid token must still be refused bob's order
  • Check ownership at every object access — deny by default

One request, two gates

One HTTP request must pass two gates. Gate 1, authentication: verify the token's signature and algorithm — broken by alg:none and a weak hardcoded secret, so a forged token passes. Gate 2, authorization: does the caller own THIS object — broken because the ownership check never runs. The key point: even a perfectly valid, genuine token clears Gate 1 but must not be enough to clear Gate 2 on its own — alice's own valid login token can still read bob's order if Gate 2 is missing.


Access control models

  • MAC — Mandatory: system-enforced labels (military)
  • DAC — Discretionary: owner grants access (file perms)
  • RBAC — Role-Based: permissions via roles
  • RuBAC — Rule-Based: conditions (time of day, IP)

Best practices: separation of duties · least privilege · implicit deny


Exercise — RBAC

RolePermissions
DeveloperRead/write Git, JIRA, run unit tests
QA EngineerRead Git, write test reports, JIRA, deploy to staging
Project ManagerRead Git, JIRA, view dashboard
InternRead Git, run unit tests
  1. Who may deploy to staging?
  2. An intern must create bug entries — which role's perms to add?
  3. List all roles that can modify the codebase.

Reading the attack in the logs

Reconstruct the kill chain from Apache logs:

GET /login.php?username=' or 1=1 limit 1; -- a&...   302   ← SQLi admin
POST /upload.php                                     200   ← backdoor
GET /uploads/backdoor.php?cmd=ls%20-l                200   ← RCE
  • %20 = space (URL-decode!) · logs let you trace what happened

CWE mapping

  • CWE-639 — IDOR / missing ownership check
  • CWE-347 — improper signature verification (alg:none forgery)
  • CWE-321 — use of a hardcoded/weak cryptographic key (the guessable HMAC secret)

Defenses

  • Server-side authorization on every request (deny by default)
  • Check ownership, not just authentication
  • Strong session tokens + proper expiry/rotation
  • Verify JWT signature, alg, exp, aud; keep secrets strong
  • RBAC / ABAC enforced centrally
  • Centralize the check — don't scatter if role==... everywhere

Tool — Burp Suite workflow

ToolUse
Proxyintercept & modify requests
Repeaterreplay/tweak one request
Intruderautomate brute-force/fuzz
Decoder/Comparerencode payloads / diff responses
  • Set scope, intercept, change a value (e.g. price=1), forward
  • Test auth, sessions, IDOR, privilege escalation

🗺️ Game — IDOR Treasure Hunt + JWT Forgery

  1. Tamper object IDs to reach another user's data (horizontal — become "bob," not admin) → each secret = a flag
  2. Forge a weak JWT two ways: alg:none, then crack the hardcoded HMAC secret
  3. Defend: switch to solution_app.py (already fixed), prove it blocks all three attacks (403 + two 401s), cite the exact fix line for each

Lab steps

📋 Worksheet 6 — labs/week06-authn-authz/worksheet.md (Part 3) · kickoff: docker compose up → http://localhost:8080

  1. Find IDOR endpoints; enumerate other users' objects
  2. Crack/forge a weak JWT (alg:none AND the weak HMAC secret — two separate graded attacks)
  3. Switch to solution_app.py; re-test: access denied
  4. Cite the exact line that fixes each of the three attacks

Deliverable

  • IDOR + JWT findings with impact
  • Citation of the fix lines in solution_app.py for all three attacks
  • Proof the forged token / cross-user access now fails
  • + Audit the AI / EiPE / Prompt Problem (see worksheet)

Key takeaways

  • Authenticate once, authorize every request
  • Never trust client-supplied IDs or tokens blindly
  • Deny by default

Questions?

Next week: Reflection & review (midterm prep)

All weeks in Software Security