# OWASP Top 10: category mitigations

Reference for the `security` skill. Each category: the risk, the recurring
failure pattern, and the concrete mitigations to check for in review.

Edition tracked: OWASP Top 10:2025. Last reviewed April 2026. The 2021
edition is still widely cited; the most material differences are renumbering
and the absorption of the 2021 A10 (SSRF) into broader categories. SSRF is
covered in depth in `ssrf-and-egress.md` regardless of which year's list a
reader is using. Consult https://owasp.org/Top10/ before a review to confirm
the list has not shifted again.

Cross-reference: many bullet items in this file point to a deeper reference
(`web-app.md`, `api-and-auth.md`, `file-and-input.md`, `ssrf-and-egress.md`,
`secrets.md`, `secrets-scan.md`, `dep-audit.md`, `infra.md`, `ai-agent.md`).
Use this file as the index; load the deeper reference when you need to act.

## A01: Broken Access Control

Failure pattern: the router authenticates but the handler forgets to authorise,
or the ORM fetches any row if the ID is known (IDOR).

Check:

- Authz is enforced in the handler, not only the router middleware.
- Every `findById` or equivalent is paired with an ownership or tenancy
  predicate (`where user_id = :current_user`).
- Role checks are positive allowlists; absence of a role = deny.
- Tests cover unauthenticated, wrong-tenant, and wrong-role callers on every
  protected path.

## A02: Security Misconfiguration

Failure pattern: frameworks shipped in debug/dev mode, default credentials
left enabled, permissive CORS, S3 bucket policies default-public, DB port
exposed.

Check:

- `DEBUG = false` (or equivalent) in every non-dev environment.
- No default/sample credentials retained (`admin:admin`,
  `postgres:postgres`).
- CORS allowlist is explicit; no `Access-Control-Allow-Origin: *` on
  credentialed endpoints; never reflect the `Origin` header without
  validation. Detail in `web-app.md`.
- Security headers present: `Content-Security-Policy`,
  `Strict-Transport-Security`, `X-Content-Type-Options: nosniff`,
  `Referrer-Policy`, `Permissions-Policy`. Detail and recommended values
  in `web-app.md`.
- Cookies use `HttpOnly`, `Secure`, `SameSite`, and `__Host-` prefix on
  session cookies. Detail in `web-app.md`.
- Cloud storage buckets are private-by-default; public access is
  explicitly documented and approved.
- Container, Kubernetes, and cloud baselines (non-root, drop caps,
  read-only rootfs, IMDSv2, workload identity) per `infra.md`.

## A03: Software Supply Chain Failures

Covers provenance, not just CVE presence. The concern is who built the artifact
and whether the build is reproducible, not only whether a known vulnerability is
listed.

Check:

- Lockfile pins exact versions; no floating ranges in production.
- Native dependency audit output is clean or findings are triaged. See
  `dep-audit.md` for the per-ecosystem command and exit-code semantics.
- New transitive dependencies are explained in the PR.
- Build artifacts are signed (Sigstore/cosign) where supported.
- SLSA Level 2+ for critical services: build provenance attestations
  generated by the build system, verifiable by consumers before deploy.
- SBOMs generated (`syft`, `cdxgen`) and stored with releases.
- Internal package names are scoped or prefixed and the namespace is
  registered in the public registry, blocking dependency-confusion uploads.
- New deps reviewed for typosquatting, post-install scripts, maintainer
  history, and download volume. See `infra.md` for CI controls
  (`--ignore-scripts`, private registry proxy).

## A04: Cryptographic Failures

Failure pattern: roll-your-own crypto, MD5/SHA1 still in use, RSA without
padding, hardcoded IVs, missing constant-time comparison, nonce reuse.

Check:

- Use maintained libraries for crypto, password hashing, token signing,
  token validation, and signature checks. No custom crypto, hash, MAC,
  or token formats.
- Password hashing: argon2id (preferred), scrypt, or bcrypt. Argon2id
  parameters per current OWASP guidance: `m = 19 MiB, t = 2, p = 1`
  minimum, or `m = 46 MiB, t = 1, p = 1`. OWASP's bcrypt minimum is
  cost ≥ 10; ABP prefers ≥ 12 when the measured login latency budget
  allows it. Server should adjust to hit ~250–500 ms per hash on
  production hardware.
- Symmetric: AEAD only (AES-GCM, ChaCha20-Poly1305); never CBC+HMAC
  hand-rolled.
- **Never reuse an AES-GCM nonce with the same key.** Reuse is a
  catastrophic key-recovery attack, not a confidentiality wobble. Use a
  CSPRNG-generated 96-bit nonce or a deterministic counter scheme bound
  per key.
- All IVs and nonces from a CSPRNG (`crypto.randomBytes`,
  `secrets.token_bytes`, `/dev/urandom`); never a timestamp, never a
  counter shared across processes without coordination.
- Asymmetric: Ed25519 for signing; RSA-2048+ with PSS or OAEP if RSA is
  required.
- Key storage: KMS-managed; no plaintext keys in env vars or code.
  Envelope encryption (DEK + KMS-wrapped KEK) for application-encrypted
  data. See `secrets.md`.
- Constant-time comparison for token, MAC, or capability equality
  (`crypto/subtle.ConstantTimeCompare`, `secrets.compare_digest` /
  `hmac.compare_digest`, `crypto.timingSafeEqual`).

## A05: Injection

Covers SQL, NoSQL, OS command, LDAP, expression-language, template, log,
and header injection. Plus the modern injection classes that often live
just outside this header: SSRF (`ssrf-and-egress.md`) and prompt injection
(`ai-agent.md`).

Check:

- SQL: parameterised queries only; no string concatenation of user input
  into SQL.
- **Identifiers** (table, column, `ORDER BY`) cannot be parameter-bound.
  Allowlist them against a fixed set; reject anything else.
- ORM raw escape hatches (`Sequelize.literal`, `Prisma $queryRawUnsafe`,
  Django `RawSQL`, ActiveRecord `find_by_sql`) need the same scrutiny as
  hand-built SQL.
- NoSQL: reject operator objects (`{$gt: ""}`, `$where`, `$regex`) at the
  body parser; bind values with explicit types, not pass-through `find`
  arguments.
- `LIKE` with user wildcards: escape `%` and `_`, or bound input length to
  prevent denial of service.
- OS: no `shell=True`, no `os.system`, no `exec` on string-interpolated
  commands; pass argv arrays. Where the language has only a string-shell
  API, build via a vetted argv-quoting library, never via interpolation.
- LDAP/XML/XPath: library-provided parameterisation; for XML see XXE in
  `file-and-input.md`.
- Logs: never interpolate user input into a log format string; pass as a
  structured field. CRLF in user input must be stripped before any log
  line is written (log injection / forgery).
- Header / response splitting (CRLF): user input interpolated into
  `Set-Cookie`, `Location`, or custom headers must be CRLF-stripped or
  rejected. Most framework header APIs do this; only the unsafe ones
  (raw bytes) are exposed.
- SSTI: templates compiled from user input = bug. The same applies to
  expression-language evaluators (SpEL, OGNL, Jinja `Environment` with
  user-controlled source).

## A06: Insecure Design

Failure pattern: the threat model was never written; the design doc assumes
the happy path.

Check:

- New endpoints have a STRIDE pass attached to the design doc.
- Rate limiting is designed before the endpoint ships, not after. See
  `api-and-auth.md` for actor-level limits.
- Privileged paths have separate authz checks, not shared middleware.
- Abuse cases are listed alongside use cases.
- Mass-assignment / over-binding considered for any endpoint that accepts
  a body and writes to persistence. See `file-and-input.md`.

STRIDE quick template (one row per asset / data flow / trust boundary):

| Asset | S (spoof) | T (tamper) | R (repudiate) | I (info) | D (DoS) | E (elevate) |
|---|---|---|---|---|---|---|

For each row, list the threat in plain English and the control that
prevents or detects it. Empty cells are permitted but require a one-line
justification ("not reachable from outside the bounded context").

## A07: Authentication Failures

Covers credential stuffing, session fixation, weak password policies, missing
MFA on privileged roles, enumeration on auxiliary flows.

Check:

- Password policy: NIST SP 800-63B-4 baseline, min ≥ 15 for
  single-factor passwords, min ≥ 8 only when the password is one factor
  in MFA; max ≥ 64; allow all printable Unicode + spaces; allow paste;
  no composition rules; no forced periodic rotation; breached-password
  check on set/change. See `secrets.md`.
- Rate limit at the actor (account, IP, device); distinct tighter limit on
  login, password reset, MFA enrol, token exchange. Pair with global
  credential-stuffing limits across the unauthenticated surface.
- Account lockout on repeated failures, with a back-off; lockout itself
  must not be a DoS amplifier (per-actor, not per-account).
- Anti-credential-stuffing controls on the login surface: CAPTCHA / proof
  of work / device fingerprint after suspicious patterns.
- **Session ID rotated on every privilege change**, not only on login;
  step-up MFA, role change, password change, account unlock all rotate.
  This blocks session fixation.
- Short idle timeout for privileged sessions (≤ 15 min); absolute max
  lifetime ≤ 24 h for sensitive sessions.
- MFA required for any admin or privileged role; phishing-resistant
  factor (WebAuthn / passkeys) preferred.
- SMS OTP is a fallback only, never sole factor.
- **Enumeration discipline applies beyond login.** Registration, password
  reset, MFA enrol, email change, "resend verification" must respond with
  the same shape and same timing whether the account exists or not. If
  email is sent, send "if an account exists" rather than confirming
  presence.

## A08: Software or Data Integrity Failures

Covers unsigned updates, deserialisation of untrusted data, CI/CD pipeline
tampering.

Check:

- Auto-update channels verify signatures before install.
- No deserialisation of untrusted input via `pickle`, Java
  `ObjectInputStream`, PHP `unserialize`, Node `vm.runInNewContext`, etc.
  Full list and language-specific safe alternatives in `file-and-input.md`.
- CI/CD secrets scoped by environment; prod secrets never loaded in PR
  builds.
- **Workflows triggered by `pull_request_target` (or analogous hooks) are
  high-risk:** they run with base-repo secrets and a PR-controlled
  checkout can exfiltrate them. Avoid; if needed, do not check out the PR
  head and isolate the workflow.
- CI uses OIDC for cloud auth; no long-lived static keys in CI secrets.
- Pin third-party CI Actions / reusable workflows by SHA, not by tag.
- Artifacts signed; consumers verify before deploy. SLSA provenance
  attestations on critical services. See `infra.md`.

## A09: Security Logging and Alerting Failures

Failure pattern: ample logs that record the wrong things, user secrets
appear, but auth denials don't.

Check:

- Log authn success/failure, authz denials, admin actions, boundary
  validation rejections, with request ID and actor, not raw payload.
- **Allowlist what is logged, do not deny-list what is redacted.** A
  redactor that strips by field name fails as soon as a new field is
  added; a serialiser that only emits a known set of fields does not.
- Never log passwords, tokens, session IDs, raw PII, full payment data.
- Alerts configured on failed-login spikes, privilege escalations, new
  admin creation, unexpected outbound connections.
- Retention meets policy. Logs in restricted, append-only storage with
  IAM gating writes is the practical bar for most apps; cryptographic
  tamper-evidence (hash-chained / Merkle log) for high-trust environments
  (financial, regulated, audit-evidence systems).
- Pair with the `observability` skill for log-shape and alert-design
  detail.

## A10: Mishandling of Exceptional Conditions

Covers information disclosure through error messages, missing auth checks in
error paths, and timing differences that enumerate valid users or tokens.

Check:

- Generic error to caller; detail only in server-side log with request ID.
- No stack traces, SQL fragments, or file paths in HTTP responses.
- **Same shape and same timing** on authn failure paths (user-not-found vs
  wrong-password). The way to achieve timing equality is to **always run
  the password hash**, even on user-not-found; short-circuiting on
  unknown user is the bug.
- Error handlers re-check auth before returning; no "fall through" on
  exception to a less-privileged path.
- Panics in privileged code paths terminate the process; they do not
  silently recover.
- Pair with the `error-handling` skill for safe error propagation and
  user-facing failure shape.
