Web Application Security Checklist

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.

Show
  1. Authorize every request on the server and deny by defaultMust

    Why it matters and how to check

    Hiding a button in the UI is not access control. Any route or API endpoint can be called directly, so the server has to decide, on every request, whether this user may do this action. Put the check in one shared place, such as middleware or a policy function, and make "no rule matched" mean deny.

    To verify, list every API route and confirm each one either calls your authorization helper or is explicitly marked public. Then sign in as a normal user and call two or three admin endpoints with curl or the browser console. Each should return 401 or 403, never data.

    Do

    • Keep one authorization helper and call it from every handler
    • Mark public routes explicitly so the default is protected

    Avoid

    • Relying on hidden links or disabled buttons as the only check
  2. 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/123 returns 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();
  3. 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=admin or isAdmin=true in 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
  4. 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.254 and 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.

  5. Allow only known origins in your CORS policyShould

    Why it matters and how to check

    A CORS policy that reflects any Origin header 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.example and check the response. Access-Control-Allow-Origin must not echo that value, and Access-Control-Allow-Credentials: true must 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