EXERCISE 3 — "Deny by default", server-side enforcement, and three rebuttals ============================================================================= "DENY BY DEFAULT": - The default answer to "may this user do this?" is NO. Access is forbidden unless a permission is EXPLICITLY GRANTED. You allowlist what's permitted rather than blocklisting what's forbidden. - WHY IT MATTERS: with deny-by-default, a NEW endpoint, a forgotten route, or a refactor is protected AUTOMATICALLY (it's denied until someone grants access), so mistakes fail CLOSED (safe). With allow-by-default, anything you forget to protect is wide open — mistakes fail OPEN (dangerous). Most broken access control is a forgotten check; deny-by-default makes "forgot" safe. WHY ACCESS CONTROL MUST BE ENFORCED SERVER-SIDE, EVERY REQUEST: - The CLIENT IS ATTACKER-CONTROLLED. Anything in the browser/app — hidden fields, disabled buttons, cookies, JS role flags, the URL, request bodies — can be modified or replayed with tools (curl, Burp, DevTools). So client- side checks are UX, not security. - Only the SERVER, using the AUTHENTICATED SESSION's identity/permissions, can make a trustworthy decision. And it must do so on EVERY request to a protected resource/action (each API call, not just the initial page load), because the attacker can call any endpoint directly in any order. THREE BAD ARGUMENTS, REBUTTED: 1. "The button is hidden for non-admins." WHY IT FAILS: hiding UI only changes what the user SEES, not what the server ACCEPTS. The attacker ignores the UI and calls the endpoint directly (POST /admin/api/deleteUser via curl/Burp). The button's visibility is irrelevant to the API. CORRECT PRINCIPLE: enforce the admin check SERVER-SIDE on the ENDPOINT itself; UI hiding is cosmetic, not access control. (Missing function-level access control, A01.) 2. "We check isAdmin from the cookie." WHY IT FAILS: the cookie is CLIENT-CONTROLLED data. An attacker sets isAdmin=true (or edits a tampered token) and your check passes. You're trusting the attacker to tell you their privileges. CORRECT PRINCIPLE: derive privileges from the SERVER-SIDE SESSION / a verified token, looked up against the user's real role in your data store — never from a client-supplied flag. (If using JWT, verify the signature and don't trust unverified claims — Auth course.) 3. "That URL isn't linked anywhere." WHY IT FAILS: security through OBSCURITY. Unlinked != unreachable — attackers find URLs by guessing common paths (forced browsing), scraping JS/sitemaps, reading logs/referers, or fuzzing. An endpoint with no check is exposed the moment someone types the path. CORRECT PRINCIPLE: every endpoint must enforce access control regardless of whether it's linked; "hard to find" is not "protected." (Forced browsing, A01.) THE COMMON THREAD: All three substitute something the attacker CONTROLS or can DISCOVER (the UI, a client value, a URL's obscurity) for a real server-side authorization decision. The fix is always the same: DENY BY DEFAULT, and on EVERY request make the access decision on the SERVER using the authenticated session's verified identity and permissions, checking rights to the SPECIFIC resource/action. Centralize it, log denials (A09), and apply least privilege. ONE-LINE TAKEAWAY: Deny by default and decide access on the server from the trusted session every request — hidden buttons, client cookies, and unlinked URLs are not access control.