4.5 Review Authentication, Session, and Access
4.5 - Review Authentication, Session, and Access Control
Overview
These findings ask who is acting, what binds the request to that actor, and what they are allowed to touch. Review session lifecycle, CSRF defenses, password handling, object-level authorization, and cookie flags together. For OAuth/OIDC/SAML protocol depth, continue in 5.1 Review Identity and Federation; for JWT issuance and JWKS, see 5.2 Review Tokens and API Trust.
The points below are the ideas this family chapter uses again and again.
- Ask who is acting, what binds the request to that actor, and what they may touch.
- Review session lifecycle, CSRF, passwords, object-level authorization, JWT-at-code-level, and cookies together.
- Authentication is not authorization; object IDs need server-side ownership checks.
- Protocol depth for OAuth/OIDC/SAML continues in Chapter 5.
- Evidence names the identity decision, missing control, impact, and a proving test.
After reading this chapter, we should be able to review authn, session, and access together and separate code-level JWT flaws from protocol issuance review.
Shared Review Model
Across every variant below, keep the same evidence habit: name the source, the sink, the missing control, the impact, and a test that would prove a fix.
Variants in This Family
Authentication and authorization
Insecure authentication lets attackers pose as another user through weak credential checks, session flaws, or forged tokens. Missing authorization lets authenticated users access other users' data or admin functions because the handler never verifies ownership or role. Authentication alone does not limit which records or APIs are reachable.
Where to look
| Signal | Where to look |
|---|---|
| Feature type | Reports, exports, refunds, role changes, config edits, cross-tenant queries |
| Authn entry points | Login, API keys, OAuth callbacks, service accounts, machine-to-machine tokens |
| Missing authz | Handlers load resources by attacker-supplied ID without ownership test |
| Client-trusted roles | isAdmin from JWT body, X-Admin header, or JSON field without server mapping |
| Implicit public | permitAll, [AllowAnonymous], missing middleware on /internal paths |
| Service bypass | Message consumers and schedulers calling repositories without authorization |
Appendix detail: Authentication and authorization code reference.
IDOR
IDOR is horizontal privilege escalation. A user authorized for object A can read or modify object B by changing an identifier in the URL, body, or header. The application performs the database or file operation but never checks ownership, tenant, or role against the target resource.
Where to look
| Signal | Where to look |
|---|---|
| Feature type | Profile edit, invoice download, order detail, file download, message view |
| Object selectors | Path variables, query strings, JSON id fields, hidden form inputs |
| ID-only lookup | findById(userSuppliedId) with no ownership filter in repository layer |
| Client-supplied owner | Create/update DTOs accepting userId, orgId, or accountId from body |
| Batch endpoints | Arrays of IDs returned without per-item authorization |
| File access | send_file(userInput) or bucket keys built from unsanitized names |
Appendix detail: IDOR code reference.
Forced browsing
Forced browsing is a form of broken access control. Attackers request paths such as /admin, /api/internal/export, or backup file names directly. If the server only checks that someone is logged in, any authenticated user may access admin functionality. Missing role or permission checks on each sensitive entry point create this gap.
Where to look
| Signal | Where to look |
|---|---|
| Feature type | Admin consoles, actuator endpoints, debug tools, importer APIs, legacy servlets |
| Login-only guard | Filter redirects when user == null but allows any authenticated user into /admin |
| Hidden routes | Sensitive @GetMapping without @PreAuthorize while public routes are protected |
| Static exposure | /actuator, /swagger, /debug, or .git served in production |
| Method gap | GET protected on admin page while POST /admin/delete lacks the same check |
| Parallel channels | Mobile API v2, GraphQL resolvers, WebSocket handlers missing admin checks |
Appendix detail: Forced browsing code reference.
Broken session management
Session management ties HTTP requests to an authenticated user. Weak implementations use predictable identifiers, expose tokens in URLs, skip rotation at login, or leave sessions valid after logout. Session fixation occurs when the application keeps the same session ID before and after login, allowing an attacker who planted that ID to inherit the victim's authenticated state.
Where to look
| Signal | Where to look |
|---|---|
| Feature type | Login, logout, remember-me, password change, role elevation, multi-device sessions |
| Session creation | getSession(true) before auth, client-supplied session IDs, short or predictable tokens |
| Cookie flags | Missing HttpOnly, Secure, or permissive SameSite=None without justification |
| Fixation risk | Same session ID before and after successful login |
| Logout gaps | Client-only cookie clear without server-side invalidation |
| Transport leaks | Session IDs in URLs, logs, referrer headers, or analytics |
Appendix detail: Broken session management code reference.
CSRF
CSRF tricks a logged-in victim's browser into sending a request the application treats as legitimate. The attacker does not need to steal the session cookie if the browser attaches it automatically. Impact may include fund transfers, profile changes, privilege grants, or deletion of data.
Where to look
| Signal | Where to look |
|---|---|
| Feature type | Transfers, email change, password update, role assignment, account deletion, settings forms |
| HTTP methods | POST, PUT, PATCH, DELETE; unsafe GET that mutates state |
| Session model | Cookie-based sessions without synchronizer tokens or custom headers |
| SPA/AJAX | JSON APIs authenticated by cookies alone; missing X-CSRF-Token header |
| Framework bypass | @csrf_exempt, csrf().disable(), omitted [ValidateAntiForgeryToken] |
| High-risk gaps | MFA disable, payout flows, admin actions without reauthentication |
Appendix detail: CSRF code reference.
Broken password lifecycle
Broken password lifecycle management lets attackers set, change, or reset credentials without proving identity. Risks include plain-text storage, weak hashing, missing current-password checks, reusable reset tokens, and MFA bypass on high-impact operations. Initial passwords left unchanged and debug logging of credentials amplify exposure.
Where to look
| Signal | Where to look |
|---|---|
| Feature type | Registration, admin provisioning, password change, reset confirm, MFA disable |
| Storage flaws | Plain text, MD5/SHA1, reversible encryption, passwords in audit tables |
| Change without auth | user_id from form body updates another account's password |
| Reset token flaws | Predictable tokens, long TTL, missing single-use invalidation, tokens in URL logs |
| Enumeration | Different responses for valid vs invalid email on reset |
| MFA bypass | Disable MFA without step-up; debug logging of passwords or reset links |
Appendix detail: Broken password lifecycle code reference.
JWT security (code-level)
A JWT carries claims the application may treat as identity and permissions. If signature verification is skipped, uses a hardcoded secret, or accepts the none algorithm, attackers can forge tokens and impersonate users or elevate privileges. Key confusion attacks swap asymmetric verification for symmetric keys when libraries are misconfigured.
Where to look
| Signal | Where to look |
|---|---|
| Feature type | API auth middleware, microservice trust, mobile backends, OAuth resource servers |
| Parse without verify | Permissive algorithms= including none, RS256→HS256 confusion, manual header parsing for key selection |
| Key material | Hardcoded "secretkey", dev keys shipped to prod, missing JWKS rotation |
| Algorithm issues | none accepted, HS256/RS256 confusion, ignoring alg header |
| Claim validation | Missing exp, iss, aud, or excessive access token lifetime |
| Storage and logout | localStorage tokens, refresh tokens without revocation, debug endpoints disabling verify |
Appendix detail: JWT security (code-level) code reference.
Insecure cookie configuration
Browsers store cookies and send them automatically on matching requests. Session cookies carry authentication state. If JavaScript can read the cookie, a cross-site scripting flaw may steal the session. If the cookie travels over HTTP, network attackers may intercept it. If SameSite is missing or too permissive, cross-site request forgery becomes easier. Overly long or persistent session cookies extend the window for stolen-token abuse.
Where to look
| Signal | Where to look |
|---|---|
| Feature type | Login success handlers, "remember me," OAuth callbacks, session refresh, logout |
| Framework config | web.xml, Spring application.yml, Django SESSION_COOKIE_*, Express session middleware |
| Missing HttpOnly | set_cookie(..., httponly=False), Cookie without setHttpOnly(true) |
| Missing Secure | Cookies set without Secure in production or behind TLS-terminating proxies |
| SameSite gaps | Cross-site flows, embedded widgets, OAuth redirects without deliberate SameSite choice |
| Overlong lifetime | Far-future Expires or Max-Age bypassing idle timeout policy |
| Logout gaps | Client cookie cleared but server-side session record still valid |
Appendix detail: Insecure cookie configuration code reference.
Worked Example (Authentication and authorization)
We walk Authentication and authorization in depth. Apply the same tracing steps to the other variants, adjusting sources and sinks from the tables above.
Sample vulnerable code (Python)
from fastapi import Depends, FastAPI, HTTPException
from sqlalchemy.orm import Session
app = FastAPI()
@app.get("/api/projects/{project_id}")
def get_project(project_id: str, db: Session = Depends(get_db)):
if not current_user_id():
raise HTTPException(status_code=401)
# Authenticated but no tenant/owner filter — any user reads any project
return db.query(Project).filter(Project.id == project_id).one()
@app.post("/admin/billing")
def update_billing(payload: dict):
# No role check; any caller who reaches the route can change billing config
billing_config.update(payload)
return {"ok": True}
Step-by-step review walkthrough
- Map authentication entry points. Login, API keys, OAuth callbacks, service accounts, and machine-to-machine tokens.
- Verify credential validation. Password hashes, MFA gates, account lockout, and consistent failure responses.
- List sensitive operations. Exports, refunds, role changes, config edits, and cross-tenant queries.
- For each operation, find the authorization check. Role, scope, resource owner, or policy engine call must appear before the action.
- Compare UI restrictions to server enforcement. Hidden buttons are not security controls.
- Review default-deny vs default-allow. New endpoints should require explicit permission annotations.
- Check service layers and background jobs. They must inherit the same authorization as HTTP controllers.
Risk Impact (Family)
Horizontal data access. Authenticated users read or modify records belonging to others when ownership checks are absent.
Administrative compromise. Missing role checks on admin routes let standard users change configuration, roles, or billing.
Tenant isolation failure. Cross-tenant queries without tenant predicates expose one customer's data to another.
Persistent unauthorized changes. Background jobs and message handlers without authz may apply attacker-supplied operations at scale.
Regulatory and contractual breach. Broken access control is a top OWASP category and a common finding in security assessments.
Fix Principles
Primary safer patterns for the worked example follow. For other languages and variant-specific fixes, use the family appendix.
Python
Filter queries by authenticated user. Use dependency injection for scope checks on every route.
from flask import Flask, g, abort
from flask_login import login_required, current_user
@app.route("/api/document/<doc_id>")
@login_required
def get_document(doc_id):
doc = db.documents.find_one({"_id": doc_id, "owner_id": current_user.id})
if not doc:
abort(404)
return jsonify(doc)
@app.route("/admin/settings", methods=["POST"])
@login_required
def save_settings():
if not current_user.has_role("admin"):
abort(403)
config.update(request.get_json())
return jsonify(ok=True)
# FastAPI pattern
@app.get("/api/document/{doc_id}")
def get_document(doc_id: str, user=Depends(require_scope("documents:read"))):
doc = repo.get_for_owner(doc_id, user.id)
if not doc:
raise HTTPException(status_code=404)
return doc
Important: Use user.has_perm in Django and query filters like Model.objects.filter(owner=request.user).
Verify During Review
Use the checklist below as a family-level pass over the variants above. Each item should map to evidence in the change under review.
- Every sensitive operation performs server-side authorization after authentication.
- Resource access validates ownership, tenant, or role—not only presence of a session.
- Admin and internal routes require explicit elevated permissions, not obscurity.
- New endpoints default to deny; public exceptions are documented and rare.
- Background workers and message handlers enforce the same rules as HTTP APIs.
- Code structure makes authentication steps distinct from authorization checks for maintainers.
- Object lookups include owner, tenant, or permission predicate tied to the authenticated principal.
- Create and update operations ignore client-supplied ownership fields or validate them against policy.
- File and export endpoints authorize each object, including batch and async jobs.
- Indirect reference tokens are bound to the user session and expire appropriately.
- Mobile, GraphQL, and internal APIs apply the same object-level checks as primary web routes.
- Enumeration risk is reduced with rate limits and consistent 404/403 responses where policy allows.
- Admin and internal paths require explicit elevated roles or scopes, not only authentication.
- Every HTTP method on sensitive resources has matching authorization checks.
- Global security configuration defaults to deny; exceptions are documented.
- Actuator, swagger, debug, and backup paths are disabled or restricted in production.
- Parallel API versions and WebSocket routes repeat the same authorization rules as primary HTTP handlers.
- UI hiding of links is supplemented by server enforcement on direct navigation.
- Session ID regenerates or invalidates on successful login to prevent session fixation.
- Session identifiers are long, random, and not accepted from URL parameters.
- Session cookies use
HttpOnly,Secure, and appropriateSameSitein production. - Idle and absolute timeouts match policy; privileged apps use shorter windows.
- Logout and account lock invalidate server-side session state, not only browser cookies.
- Role, tenant, or MFA changes trigger session rotation or reauthentication where required.
- Session IDs do not appear in logs, analytics, or external referrer URLs.
- Every state-changing endpoint validates CSRF tokens or equivalent framework protection.
- Session cookies use
Secure,HttpOnly, and appropriateSameSiteattributes. - No sensitive mutations over GET; dangerous actions require POST or API verbs with protections.
- SPAs and AJAX include anti-forgery headers when cookies authenticate requests.
- High-risk operations add reauthentication, MFA, or CAPTCHA beyond generic CSRF tokens.
- CSRF protections are not globally disabled in security configuration without documented exceptions.
- Passwords are hashed with modern algorithms and unique salts; plain text never stored or logged.
- Password change requires current credential or fresh reauthentication tied to the active session.
- Reset tokens are random, hashed at rest, time-limited, single-use, and not derived from email alone.
- Reset and registration responses do not enumerate valid accounts to anonymous callers.
- MFA is required for enrollment removal, email change, and other high-risk account operations.
- Initial and temporary passwords force change on first login when policy requires it.
- Audit trails record lifecycle events without cleartext passwords or reset secrets.
- Every authentication path verifies JWT signature with the correct key and algorithm.
noneand unexpected algorithms are rejected; asymmetric and symmetric paths are not confused.exp,iss, andaud(andnbfwhen used) are validated with acceptable clock skew.- Signing keys are not hardcoded in production; rotation and JWKS are supported where applicable.
- Authorization uses claims from verified tokens only, not duplicate client-controlled headers.
- Access token lifetime matches risk; refresh and logout invalidate continued use when required.
- Session and authentication cookies set
HttpOnlyandSecurein production. SameSiteis chosen deliberately for CSRF and OAuth redirect flows;NonerequiresSecure.- Cookie lifetime matches session timeout policy; "remember me" uses separate, scoped cookies when enabled.
- Logout invalidates server-side sessions and clears client cookies.
- Framework defaults were verified, not assumed; deployment descriptors match application code.
- No sensitive session identifiers appear in URL query parameters as a substitute for cookies.
Code Reference (Appendix)
Payloads, language-specific sinks, multi-language examples, and full fix catalogs for every variant live in 4.5 code reference — Review Authentication, Session, and Access Control.
Reference
-
Appendix — 4.5 code reference
- OWASP Top 10 — Broken Access Control
- OWASP Authorization Cheat Sheet
- Django — Permissions
- Flask-Login documentation
- Spring Security — Method Security
- ASP.NET Core — Authorization
- Casbin documentation
- CWE-639: Authorization Bypass Through User-Controlled Key
- OWASP IDOR Prevention Cheat Sheet
- Django — Object-level permissions
- Spring Data — Query methods
- Spring Security — PostAuthorize
- ASP.NET Core — Resource-based authorization
- EF Core — Global query filters
- CWE-425: Direct Request ('Forced Browsing')
- Django — Permissions and authorization
- Flask-Principal roles
- Spring Security — Authorize HTTP requests
- ASP.NET Core — Policy-based authorization
- CWE-384: Session Fixation
- CWE-613: Insufficient Session Expiration
- OWASP Session Management Cheat Sheet
- OWASP ASVS — Session Management
- Django — Using sessions
- Flask — Sessions
- Java Servlet — changeSessionId
- ASP.NET Core — Session
- gorilla/sessions
- CWE-352: Cross-Site Request Forgery
- OWASP CSRF Prevention Cheat Sheet
- Django — CSRF protection
- Flask-WTF CSRFProtect
- Spring Security — CSRF
- ASP.NET Core — Prevent cross-site request forgery
- gorilla/csrf package
- MDN — SameSite cookies
- CWE-620: Unverified Password Change
- CWE-640: Weak Password Recovery Mechanism
- OWASP Forgot Password Cheat Sheet
- OWASP Password Storage Cheat Sheet
- NIST SP 800-63B: Digital Identity Guidelines
- passlib documentation
- Spring Security — Password Storage
- ASP.NET Core Identity
- Go golang.org/x/crypto/bcrypt
- CWE-347: Improper Verification of Cryptographic Signature
- CWE-287: Improper Authentication
- RFC 7519: JSON Web Token
- OWASP JWT Cheat Sheet
- PyJWT documentation
- Auth0 — JWT algorithm confusion
- jjwt library
- ASP.NET Core — JWT Bearer authentication
- golang-jwt/jwt v5
- MDN Set-Cookie
- RFC 6265: HTTP State Management Mechanism
- Flask session cookie configuration
- Django session settings
- Jakarta Servlet Cookie API
- Spring Boot server.servlet.session.cookie
- ASP.NET Core cookie authentication
- Go net/http Cookie