Most of the security incidents we have cleaned up were not clever attacks. They were an endpoint that forgot to check ownership, a debug page left on, a dependency two years behind, or an error handler that printed a stack trace with a connection string. All of them would have been caught by a boring checklist run a week before launch, by someone who was not the author of the feature.

This is the checklist we use. It is organised by the categories of the OWASP Top 10:2025, the edition that replaced the 2021 list, so that findings map to a shared vocabulary for reviewers and auditors. Each item is a check you can run or automate, not a principle. It is aimed at small and medium teams shipping a typical web app: a server-rendered or SPA frontend, a JSON API, a relational database and a cloud host.

How to run it

Block half a day, two people, and a staging environment that mirrors production config (same headers, same auth provider, same build flags). One person drives, the other writes findings into the tracker with a severity and owner. Anything rated high blocks launch; everything else gets a date. Repeat the automated parts in CI so the checklist does not decay after launch day.

A01 Broken Access Control

Still number one, and in 2025 it also absorbs server-side request forgery. This is where we find the most real bugs.

  • For every endpoint that takes an ID, log in as user A, request user B's resource, and expect 404 or 403. Do it for read, update, delete and export endpoints, not just the obvious ones.
  • Check that authorization lives in one layer (policies, guards, scoped queries) instead of being scattered through controllers.
  • Confirm admin routes are denied for non-admins at the server, not just hidden in the UI.
  • If the app fetches URLs supplied by users (webhooks, link previews, image import), block private and link-local ranges and the cloud metadata address.

An automated cross-tenant test is cheap and catches most regressions:

def test_cannot_read_other_tenant_invoice(client, invoice_of_tenant_b, user_of_tenant_a):
    client.force_login(user_of_tenant_a)
    r = client.get(f"/api/invoices/{invoice_of_tenant_b.id}/pdf")
    assert r.status_code == 404

A02 Security Misconfiguration

Misconfiguration moved up to second place. Most of it is default settings that were fine on a laptop.

  • Debug mode off, framework error pages off, directory listing off.
  • Default admin accounts and sample data removed; admin panels behind SSO or an IP allowlist.
  • Cookies: Secure, HttpOnly for session cookies, an explicit SameSite.
  • TLS only, with HSTS once you are sure every subdomain serves HTTPS.
  • Security headers present on every response, including error pages:
curl -sI https://staging.example.com/ | grep -iE \
  'strict-transport-security|content-security-policy|x-content-type-options|referrer-policy|frame-ancestors|x-frame-options'

Content Security Policy deserves its own pass. Start from a deny-by-default policy and add the hosts you actually load from. Run it in Content-Security-Policy-Report-Only for a week in staging and fix every violation before you enforce it:

add_header Content-Security-Policy "default-src 'self'; script-src 'self' https://js.stripe.com; frame-src https://js.stripe.com; img-src 'self' data:; object-src 'none'; base-uri 'self'; frame-ancestors 'self'" always;

The always flag matters: without it nginx omits the header on 4xx and 5xx responses.

A03 Software Supply Chain Failures

New in this edition, and broader than "outdated components": it covers dependencies, build systems and the pipeline that publishes your artifacts.

  • Lockfiles committed, and CI installs with npm ci, composer install or pip install --require-hashes so builds use exactly what was reviewed.
  • Dependency scanning on every pull request (npm audit --omit=dev, composer audit, pip-audit, or the platform's built-in scanner), with criticals failing the build.
  • CI tokens scoped to least privilege; third-party CI actions pinned to a commit SHA, not a tag.
  • Base images pinned and rebuilt on a schedule so OS packages get patched.
  • Someone owns the dependency update queue; an unread bot backlog is the same as no bot.

A04 Cryptographic Failures

  • Passwords hashed with Argon2id or bcrypt through the framework's helper, never a fast hash.
  • No secrets in the repository, container images or frontend bundles. Grep the built JS for key patterns, because build-time env vars end up in bundles more often than people expect.
  • Personal data encrypted at rest where your provider supports it, and backups included.
  • Tokens for password reset and email verification are random, single use and short lived.

A05 Injection

  • Every query uses bound parameters or the ORM. Search for string-built SQL, especially ORDER BY, LIMIT and column lists built from request input.
  • Templates auto-escape; every "raw" or "unescaped" output is reviewed and justified.
  • No shell commands built from user input; if unavoidable, pass arguments as an array.
  • Rich text is sanitised on the server with an allowlist sanitiser.

A06 Insecure Design

Design flaws do not show up in scanners, so this is a short conversation rather than a tool run.

  • Rate limits on login, signup, password reset, OTP and any endpoint that sends email or SMS.
  • Business limits enforced server side: coupon reuse, negative quantities, refund amounts.
  • A short threat model for money-moving and account-changing flows: who can trigger it, what is the worst outcome, what stops it.

A07 Authentication Failures

  • Session ID rotated on login and privilege change; logout invalidates the session server side.
  • MFA available, and required for admins.
  • Login errors identical for unknown user and wrong password; password reset response the same whether or not the email exists.
  • Checks against known breached passwords at signup and password change.

A08 Software or Data Integrity Failures

  • Webhooks verify signatures and reject stale timestamps before doing any work.
  • No deserialisation of untrusted data into native objects (PHP unserialize, Python pickle, Java native serialisation).
  • Scripts from CDNs either self-hosted or loaded with Subresource Integrity.
  • Release artifacts built in CI, not on a laptop, and deployed by digest.

A09 Security Logging and Alerting Failures

The 2025 name stresses alerting: logs nobody reads do not count.

  • Log authentication events, authorization denials, admin actions and payment changes with user ID, IP and request ID.
  • Never log passwords, tokens, full card numbers or session cookies; test this by grepping staging logs after a full sign-up and checkout run.
  • At least one alert wired to a human: a spike in 403s, failed logins or 5xx.
grep -RInE 'password=|authorization: bearer|set-cookie' /var/log/app/ | head

A10 Mishandling of Exceptional Conditions

Also new. It covers errors that leak information and errors that leave the system in an unsafe state.

  • A global error handler that returns a generic message and a request ID, never a stack trace.
  • Fail closed: if the permission service, feature flag service or payment provider times out, deny rather than allow.
  • Partial failures in multi-step operations roll back or are retried idempotently.
  • Timeouts set on every outbound call so one slow dependency cannot exhaust workers.

Common mistakes

Mistake Why it hurts Do this instead
Running only an automated scanner Scanners rarely find broken access control or logic flaws Pair scans with manual cross-user tests
Enforcing CSP on launch day Breaks payment widgets or analytics in production Report-only in staging first, then enforce
Treating the checklist as one-off Settings drift with every release Automate headers, scanning and authz tests in CI
Letting the feature author self-review Same blind spots that wrote the bug Have a second person drive the checklist
Logging everything "for debugging" Secrets and personal data end up in log storage Allowlist fields in structured logs

Key takeaways

  • Use the current OWASP Top 10 (2025) categories as headings so findings map to a shared language.
  • Broken access control is the category most likely to bite; test it with two real accounts.
  • Supply chain and exceptional-condition handling are now first-class items, not footnotes.
  • Turn every check that can be automated into a CI job; keep the manual pass short and focused.
  • Assign a severity and an owner to every finding, and block launch on highs.

References