A03: Injection
Untrusted input reaching something that interprets structure, a SQL query, a shell, a browser's HTML parser, instead of being treated as inert data the whole way through.
- All SQL queries use parameterized queries or an ORM's query builder, never string formatting. detail
- Dynamic table or column names (sort order, filters) are checked against a hardcoded allowlist before touching a query. detail
- Shell commands are invoked with an argument list, never a single interpolated string handed to a shell. detail
- All user-supplied content rendered into HTML is escaped by the template engine's default, not manually. detail
- Every dangerouslySetInnerHTML, v-html, or [innerHTML] usage has been reviewed for a path from user input. detail
- A Content-Security-Policy header is set and blocks inline scripts as a backstop. detail
A01: Broken Access Control
Checking that a request is authenticated is not the same as checking that it's authorized. This category, and CSRF alongside it, covers every place those two get confused.
- Every object lookup by ID is scoped to the authenticated user, not just checked for a valid session. detail
- Admin-only and privileged routes have their own authorization check, separate from the login check. detail
- No authorization decision is made only in frontend code; every sensitive action is enforced again on the server. detail
- Access-denied responses default to a 404 unless the resource's existence is not itself sensitive. detail
- Every state-changing action uses POST, PUT, PATCH, or DELETE, never GET. detail
- State-changing form endpoints validate a CSRF token, and session cookies set SameSite=Lax or stricter. detail
A07: Identification and Authentication Failures
Login is the highest-value target in the app. Everything here is about what happens around a correct password, not just whether one was entered.
- Login endpoints are rate-limited per account and per source. detail
- The session ID is regenerated on login, logout, and password change. detail
- Password reset tokens are cryptographically random, single-use, and expire in under 30 minutes. detail
- Any link emailed to a user is built from a hardcoded host, never the request's Host header. detail
- Access tokens expire in minutes, not days; refresh tokens are revocable and rotated on use. detail
A02: Cryptographic Failures
Data that was "encrypted" in name only: the wrong hash function, a key sitting next to the data it protects, or a certificate check quietly turned off.
- Passwords are hashed with bcrypt or argon2, never a general-purpose hash like SHA-256 or MD5. detail
- Sensitive fields held on behalf of users are encrypted at rest with a key stored outside the database. detail
- No .env or secrets file has ever been committed to git; secrets load from the environment or a secrets manager. detail
- No code path sets verify=False, or the equivalent, on an HTTPS request. detail
- Signing and encryption keys are unique per environment, never hardcoded, and rotated on a schedule. detail
A05: Security Misconfiguration
Nothing exploited, a default just did the attacker's job for them.
- Debug mode and verbose stack traces are disabled outside local development. detail
- No service (Redis, database, admin panel) is reachable with a default or blank credential. detail
- CORS responses use an explicit origin allowlist, never a reflected Origin header, whenever credentials are allowed. detail
- Cloud storage buckets holding user data default to private, with access mediated by the application. detail
- Deployments ship only the built artifact, never the .git directory or the full repository. detail
A08: Software and Data Integrity Failures
Trusting a byte stream or a package registry without verifying either.
- No code path deserializes untrusted input with pickle, ObjectInputStream, unserialize(), or Marshal.load. detail
- CI installs dependencies from a committed lockfile, not an unpinned version range. detail
- Any curl-into-bash style install step verifies a checksum before executing. detail
- CI secrets are scoped to the specific job that needs them, not available to every step by default. detail
A06: Vulnerable and Outdated Components
The vulnerability was never in code your team wrote.
- A software bill of materials or equivalent inventory exists for every deployed service, including transitive dependencies. detail
- Dependency scanning runs on every build and fails it on a high-severity match. detail
- Dependency updates arrive continuously in small pull requests, not once a year in one large migration. detail
- Any dependency that can't be upgraded immediately has a compensating control and a tracked, dated plan to upgrade. detail
A04: Insecure Design
Not one bug but a habit, threading through every article in this series: assume a check will eventually be missed, and design so the blast radius stays small when it is.
- New routes default to requiring authorization; access is granted explicitly, not assumed open. detail
- Database accounts and service credentials hold only the permissions their specific job requires. detail
- High-impact actions (large transfers, account deletion, email changes) require a confirmation step beyond a single authenticated request. detail
- Security-relevant decisions live in one centralized function or middleware, not copy-pasted per handler. detail
A10: Server-Side Request Forgery
A server that fetches a URL somebody else supplied, and no check on where that URL is allowed to point.
- Any feature that fetches a user-supplied URL validates the resolved IP against private and internal ranges, not just the hostname. detail
- That validation re-checks each redirect hop rather than trusting the original URL alone. detail
- Cloud instances enforce IMDSv2 or the equivalent token-gated metadata API. detail
- Servers that fetch external URLs have no network route to internal-only services or the metadata endpoint unless explicitly required. detail
A09: Security Logging and Monitoring Failures
Every other item on this checklist assumes a failure will slip through eventually. This category is whether you'd actually notice when one does.
- Every authentication attempt, authorization failure, and admin action logs actor, action, outcome, and source. detail
- No log line contains a raw password, full token, or full card number; fields are allowlisted explicitly. detail
- Alerting rules exist for account-specific and distributed login-failure spikes, not just a generic error rate. detail
- Log retention covers a realistic detection window, and alerting rules are tested in a drill at least twice a year. detail
How to actually use this
Not every item applies to every app, a static marketing site has no login to rate-limit and no database to inject into. Go through each category against your own system honestly, and where an item doesn't hold, follow the "detail" link back to the article it came from. Every one of them shows the actual vulnerable code, the exploit, and the fix, not just the one-line summary sitting here.
This list is worth rerunning periodically, not just once. A codebase that passed every item here on the day it launched can drift out of compliance with itself: a new endpoint added without the access check the rest of the app has, a dependency quietly falling multiple majors behind, a debug flag flipped on to chase down a production bug and never flipped back. Treat this as a recurring audit, not a one-time certificate.