Chapter 1 of 8 · 5 checks · 3 min
Access control (A01)
Broken access control is the top risk in the 2025 list. These checks make sure every request is authorised on the server, for the right user and the right object.
Check ownership of every object a user reads or changesMust
Why it matters and how to check
Insecure direct object references are the most common access control bug:
GET /api/invoices/123returns invoice 123 to anyone signed in, because the code checks login but not ownership. Every query that loads a record by id should also filter by the current user, tenant or team.To verify, sign in as user A and copy the id of one of their records. Sign in as user B in another browser and request that id directly, for read, update and delete. All three must fail. Repeat for nested resources, such as comments on a post, where the parent check is often forgotten.
Example
TypeScript // Scope the query to the owner, not just the id const invoice = await Invoice.findOne({ _id: id, ownerId: session.userId }); if (!invoice) return notFound();Protect admin and internal functions with a role checkMust
Why it matters and how to check
Admin screens, internal tools and debug endpoints are often added quickly and protected only by an obscure URL. Attackers find them through JavaScript bundles, sitemaps and guessing. Each one needs a server-side role check, and the role must come from your database or a signed token, never from a request parameter or a client-set cookie.
To verify, search your client bundle and route list for paths containing admin, internal or debug. Call each with a normal user account and with no account. Also try sending
role=adminorisAdmin=truein the request body of profile update endpoints and confirm the stored role does not change.Avoid
- Accepting role or permission fields from the client on update endpoints
Validate URLs your server fetches on behalf of usersShould
Why it matters and how to check
Server-side request forgery is now part of A01. If your app fetches a URL the user supplied, such as an avatar link, a webhook or a link preview, an attacker can point it at internal addresses like cloud metadata services or your database admin port.
Allow only the schemes and hosts you need, resolve the hostname and reject private, loopback and link-local addresses, and do not follow redirects blindly. To verify, submit
http://127.0.0.1,http://169.254.169.254and a URL that redirects to one of them. Each request should be refused before any connection is made, and the refusal should appear in your logs.Allow only known origins in your CORS policyShould
Why it matters and how to check
A CORS policy that reflects any
Originheader while also allowing credentials lets any website read your users' data through their browser. Keep an explicit allowlist of exact origins, and avoid loose patterns that would also match a subdomain an attacker could take over.To verify, send a request with
Origin: https://evil.exampleand check the response.Access-Control-Allow-Originmust not echo that value, andAccess-Control-Allow-Credentials: truemust never appear alongside a wildcard or reflected origin.Example
Shell curl -s -I https://your.app/api/me -H "Origin: https://evil.example" | grep -i access-control