# OWASP ASVS L2 + API Security controls

Implementation map for the highest-gap controls hardened in this tree. Not a certification claim — evidence of engineering against ASVS 5.0 Level 2 and OWASP API Security Top 10.

| Control ID | Area | What shipped |
| --- | --- | --- |
| **V4.1.1 / V4.1.3** · **API1:2023** | Authz | `POST …/public/preview-token` requires authenticated mutate role; verify stays public. Opt-in `host.publicAbac` denies public content reads with `ABAC_DENIED` receipt + `abac.denied` audit (assets/GraphQL out of slice; off by default). |
| **V4.2.2** · **API2:2023** | CSRF | Double-submit HMAC CSRF on admin/`/api/me`/preview-token mint for session-cookie requests; pure Bearer clients skip CSRF; session+Bearer still enforces. |
| **V11.1.4** · **API4:2023** | Rate limits | Existing per-caller/category limits; responses now expose `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` (+ `Retry-After` on 429). |
| **V14.4.1–V14.4.3** · **API8:2023** | Secure headers | Existing nosniff / DENY / Referrer / CSP / Permissions-Policy; added `Cross-Origin-Opener-Policy: same-origin` and `Cross-Origin-Resource-Policy` (`same-origin`, `cross-origin` on public/GraphQL). |
| **V5.1 / V12.1.1** · **API3:2023 / API4:2023** | Input validation | JSON bodies capped at 1 MiB (`413 PAYLOAD_TOO_LARGE`); preview-token body fields length-bounded via TypeBox. |
| **V2.3.1 / V14.1.3** · **API8:2023** | Secret handling | Fail-closed `resolvePreviewSigningSecret` / `resolveHmacSigningSecret`; removed hardcoded `edgecms_*_secret` fallbacks; no API-key reuse as HMAC material. |

## Checks

```bash
bun test --isolate \
  apps/api/src/tests/auth/secrets.test.ts \
  apps/api/src/tests/auth/csrf.middleware.test.ts \
  apps/api/src/tests/auth/authorization.test.ts \
  apps/api/src/tests/auth/rate-limit.middleware.test.ts \
  apps/api/src/tests/main.test.ts
```

See also [Authentication and access](auth-and-access.md) and [Operations and security](operations-security.md).
