Secure Coding II: Authentication, Access Control, and Secrets
CAP, ACID vs BASE, latency numbers, back-of-envelope estimation, single points of failure — the vocabulary every system designer thinks in.
Core Philosophy: The bugs in 2.6, 2.7, and 2.8 — broken authentication, broken access control, request forgery — are different in nature from injection. They are not “data confused as code.” They are design failures: missing checks, misplaced trust, weak processes. You cannot encode your way out of them. They are fixed by applying correct patterns, consistently, and by putting trust decisions in the right place: the server.
Part 1: The Problem
Page 4.1 defended the injection family — bugs with a clean structural cure. This page defends the other major Phase 2 attack classes, which have a different character.
Broken authentication (2.6), broken access control (2.7), and request forgery (2.8) are logic and design failures. There is no single line to rewrite. The fixes are correct patterns applied with discipline: design authentication properly, check authorization on every request, verify that requests are intentional, keep secrets out of reach. One principle unifies all of it — enforce trust decisions server-side, because the client is fully attacker-controlled.
Part 2: Defending Authentication
Page 2.6 attacked credentials and sessions. Defending authentication means hardening every piece it attacked — the “preview of 4.2” from 2.6, built out.
Strong password practices.
- Enforce reasonable password strength, and check passwords against lists of known-breached passwords, rejecting them — a breached password is the exact ammunition for credential stuffing (2.6).
- Avoid counterproductive rules (absurd maximum lengths, forced frequent rotation) that push users toward weak, predictable choices — usability is part of security (1.1).
- Store passwords correctly — non-negotiable: salted, with a slow, purpose-built password-hashing function (bcrypt, scrypt, Argon2), exactly as in 1.3. Never plaintext, never a fast hash, never reversible “encryption.”
Multi-Factor Authentication (MFA). The highest-impact authentication control (1.4, 2.6). Even a correct stolen password fails without the second factor — MFA defeats credential stuffing, password spraying, and basic phishing. Offer it, encourage it, require it for sensitive and administrative accounts.
Protect against automated attacks. Implement rate limiting and account lockout (or progressive delays / challenges) so brute force and stuffing are not viable — applied consistently to the login, the password-reset flow, and any API authentication. Attackers probe for the one endpoint that lacks protection.
Robust password reset. The reset flow is often the weakest link (2.6). Reset tokens must be random, single-use, and expiring; the flow must not reveal whether an account exists; avoid relying on guessable security questions.
Uniform responses. Do not let messages or response timing reveal whether a username exists — that is the username-enumeration leak from 2.6.
Part 3: Defending Sessions and Tokens
The session is the proof-of-login the user carries afterward — and 2.6 showed it is attacked independently of the password.
Strong session management:
- Session identifiers must be long, random, and unpredictable — never sequential or guessable (defeats the predictable-token attack from 2.6).
- Issue a fresh session at login. Always generate a new session identifier when a user authenticates — this defeats session fixation (2.6).
- Proper expiry and logout. Sessions must time out sensibly, and logout must invalidate the session server-side — not merely drop it from the browser (the 2.6 lab: a logged-out session that still works server-side is a live hole).
- Protect the session cookie. Mark it
Secure(sent only over HTTPS, so it cannot be sniffed — 1.3) andHttpOnly(unreadable by JavaScript, blunting XSS cookie theft — 4.1). ConfigureSameSite(which also helps against CSRF — Part 4).
Correct token handling (for token-based auth / JWTs, from 1.4):
- Tokens must be properly signed, and the server must always verify the signature — an unverified or weakly-signed token can be forged (the 2.6 attack).
- No secrets in the token payload — a JWT payload is only encoded, fully readable (the 1.4 lab). The signature prevents tampering, not reading.
- Tokens need sensible expiry, and a plan for revocation where the design requires it.
Whatever the browser holds — session ID or token — is the user’s identity (1.4). Defending sessions means making that credential unpredictable, protected in transit, properly expired, and (for tokens) unforgeable.
Part 4: Defending Access Control
Broken access control (2.7) was consistently among the most common, most impactful bug classes — a bug of omission: the missing check. Defending it is disciplined, consistent enforcement.
Deny by default. Access is refused unless a rule explicitly grants it. Many apps wrongly default to allow; flip that.
Enforce every check server-side. The single most important rule. The browser, hidden fields, disabled buttons, and client-supplied values are all attacker-controlled (2.7). The server, holding the authenticated identity, makes every access decision, on every request. Hiding a button is not access control.
Verify ownership, not just authentication. The direct fix for IDOR (2.7): when a request asks for a specific object, the server must confirm the authenticated user actually owns or may access that specific object. “Is someone logged in?” is not enough; “may this user access this object?” is the question.
Check function-level permissions. Every sensitive action and endpoint independently verifies the user’s role and rights — regardless of what the UI shows. The admin endpoint behind the hidden admin button must itself reject non-admins (the missing-function-level-control bug from 2.7).
Centralize access-control logic. One well-tested, consistently-applied mechanism beats hundreds of scattered ad-hoc checks — scattered checks inevitably get forgotten, and a forgotten check is the vulnerability.
Do not rely on obscurity. Hard-to-guess identifiers are fine as an extra layer, but they are not access control — the ownership check is the control (2.7).
Log access-control failures. Repeated “access denied” events signal an attacker probing — feed them to monitoring (4.6).
Part 5: Defending Against Request Forgery
Page 2.8’s CSRF and SSRF both abuse automatic trust; the defense is to stop trusting automatically.
Defending against CSRF — make the server able to tell an intentional request from a forged one:
- Anti-CSRF tokens — the genuine site embeds a unique, unpredictable value in its forms/requests; the server validates it. An attacker’s off-site page cannot know the token, so its forged request fails. Use the framework’s built-in mechanism — do not hand-roll it.
SameSitecookies — configuring session cookies so the browser limits sending them on cross-site requests, cutting off the mechanism CSRF depends on.
Defending against SSRF — never let untrusted input freely dictate what the server fetches:
- Allowlist destinations — permit only specific, known, intended URLs/hosts. Prefer allowlisting over blocklisting (blocklists of “bad” addresses are endlessly bypassable).
- Block internal targets — refuse requests to internal IP ranges,
localhost, and cloud metadata endpoints (the high-value SSRF targets from 2.8). - Isolate and limit the URL-fetching component — segmentation and least privilege (4.3/4.4) so even a successful SSRF reaches little.
- Do not reflect raw fetch responses back to the user.
Part 6: The Unifying Principle — Trust the Server, Not the Client
Step back and one principle ties this entire page — and much of secure coding — together:
Every security-relevant decision must be made and enforced where the attacker cannot reach: on the server. The client is fully attacker-controlled.
You have now seen this from many angles: access control enforced server-side, not by hiding UI (2.7); input validation on the server (2.7, 4.1); authorization never trusting client-supplied role values (2.7); anti-CSRF tokens because the server must verify intent (2.8); logout invalidating the session server-side (2.6). The browser, the mobile app, the API client, the cookie, the hidden field — the attacker controls all of it (exactly what Burp, 2.2, demonstrated). The server is the one place the attacker cannot rewrite.
A second unifying principle — manage secrets properly. Passwords, API keys, tokens, encryption keys: never hard-coded in source, never committed to repositories (the leaked-secrets finding from 2.1 and 2.10), never exposed in client-side code or readable config. Use proper secrets-management mechanisms; keep secrets server-side and access-controlled. (Secrets handling recurs in Phase 5’s DevSecOps track.)
🔑 The deep lesson: 4.1’s bugs were cured by construction; this page’s bugs are cured by discipline and correct placement of trust. Design authentication well, check authorization on every request, verify intent, guard secrets — every one of those decisions made server-side.
📓 Key Terms
| Term | Plain meaning |
|---|---|
| MFA | Multi-Factor Authentication — the highest-impact authentication control. |
| Rate limiting / lockout | Restricting login attempts to defeat brute force and stuffing. |
| Session management | Correct handling of session identifiers — random, fresh at login, properly expired. |
| Anti-CSRF token | An unpredictable value proving a request came from the genuine site. |
| Deny by default | Access refused unless explicitly granted. |
| Ownership check | Verifying the authenticated user may access this specific object — the IDOR fix. |
| Function-level access control | Permission checks on the actual endpoints, not just the UI. |
| Secrets management | Properly storing and protecting passwords, keys, and tokens. |
| Server-side enforcement | Making security decisions where the attacker cannot reach. |
🧪 Hands-On Lab
Use the deliberately vulnerable apps from Phase 2 (vulnerable + fixed versions) and small apps of your own. Goal: build defenses and verify they hold against your own Phase 2 attacks.
Task 1 — Revisit your Phase 2 findings. Open your notes from 2.6, 2.7, 2.8 — the broken-auth, IDOR, and CSRF/SSRF bugs.
Task 2 — Harden authentication. On a small app: enforce reasonable password strength, ensure passwords are stored salted with a slow password-hashing function, add rate limiting to the login. Re-run a brute-force-style attack from 2.6; observe it throttled.
Task 3 — Fix session handling. Ensure a fresh session ID is issued at login (test session fixation now fails); ensure logout invalidates the session server-side (re-run the 2.6 logout test); set Secure and HttpOnly on the session cookie.
Task 4 — Fix an IDOR. Take an IDOR from 2.7. Add a server-side ownership check. Re-run your 2.7 cross-account attack; confirm it now returns “denied.”
Task 5 — Fix function-level access control. Take an admin action a normal user could reach in 2.7. Add a server-side role check on the endpoint itself. Re-run via Burp (bypassing the UI); confirm the server rejects the non-admin.
Task 6 — Add CSRF protection. On a state-changing action, add anti-CSRF token protection and configure SameSite. Re-test the 2.8 CSRF scenario; confirm the forged request fails.
Task 7 — Mitigate SSRF. Take the SSRF feature from 2.8. Add destination allowlisting and block internal/metadata targets. Re-test; confirm the server refuses attacker-chosen internal URLs.
Task 8 — Extend your secure-coding notes. Add authentication, sessions, access control, CSRF/SSRF, and secrets management to “Secure Coding Patterns” — each with the correct pattern and a before/after.
⚠️ Common Mistakes
- Enforcing access control in the UI. Hiding buttons is not security — the attacker bypasses the browser. Every check runs server-side.
- Checking authentication but not ownership. “Is someone logged in?” is not “may this user access this object?” The ownership check is the IDOR fix.
- Storing passwords wrongly. Plaintext, fast hashes, or reversible encryption are all wrong. Salted + slow purpose-built password-hashing function, always.
- Protecting login but not reset or API auth. Attackers find the unprotected endpoint. Apply protections consistently across all authentication paths.
- Hand-rolling CSRF protection or session logic. Use the framework’s tested mechanisms (the 1.3 “don’t roll your own” lesson).
- Hard-coding secrets / committing them to repos. A real, common finding (2.1, 2.10). Use proper secrets management.
- Scattering access-control checks ad hoc. Forgotten checks become vulnerabilities. Centralize the logic.
✅ Recap & What’s Next
- Broken authentication, access control, and request forgery are design failures — cured by correct patterns applied with discipline, not by encoding.
- Harden every piece: strong passwords + MFA + rate limiting + robust resets; random, fresh, properly-expired sessions and verified tokens; deny-by-default access control with server-side ownership and function-level checks; anti-CSRF tokens and SSRF allowlisting.
- The unifying principle: every security decision is enforced server-side, and secrets are managed properly, never hard-coded or exposed.
Next (4.3): Secure coding fixes the code. But security must also be designed into the architecture — the structure of the whole system — before any code is written.
⁂ Back to all modules