# OWASP API Security Top 10 mapping

> This page maps each OWASP API Security Top 10 category to the relevant DaloyJS control. The mapping also names the policy and infrastructure work that remains outside the framework.

The [OWASP API Security Top 10 (2023)](https://owasp.org/API-Security/editions/2023/en/0x11-t10/) is the canonical checklist for what attackers actually exploit against HTTP APIs. This page maps every item to the Daloy primitive, middleware, or boot guard that addresses it, including the cross-cutting best practices called out in the [Aikido 2025 API security guide](https://www.aikido.dev/blog/api-security-guide) (encryption, validation, rate limiting, logging, inventory, third-party API safety).

Daloy's posture is **secure-by-default**: dangerous choices are refused at construction or boot, the rest is one documented call away. Items marked *your responsibility* are the ones no framework can decide for you (business-logic authorization, data sensitivity classification, threat modelling).

**Diagram: Where each item is handled**

- **Refused at construction or boot** - the dangerous choice never makes it to runtime - [cors origin: '*', weak session secret, HS* under 32 bytes, session() without csrf()]
- **Enforced per request by core + middleware** - one documented call away - [bearerAuth() / jwk(), requireScopes(), rateLimit(), fetchGuard(), secureHeaders()]
- **Still your responsibility** - decisions no framework can make for you - [object ownership checks, data classification, abuse threat modelling]

The OWASP API Top 10 splits cleanly across three layers in Daloy: choices refused before runtime, controls one call away, and the business-logic decisions only your code can make. The mapping table below walks each item through these layers.

## The mapping

| OWASP API risk | What Daloy gives you | Still your job |
| --- | --- | --- |
| **API1: Broken Object Level Authorization (BOLA)**. Attacker swaps an `id` in the URL to read someone else's record. | Per-route auth via `bearerAuth()` / `basicAuth()` / `requireScopes()`; typed `ctx.state.auth` contract so the handler always knows who the caller is; Standard Schema params let you validate ID shape; `onAuthSuccess` hooks for attaching tenant/user context. | The actual owner or tenant constraint inside the data operation. No framework can know your ownership model. Follow the [resource authorization guide](/docs/security/resource-authorization) and prove it with cross-user tests. |
| **API2: Broken Authentication** | `createJwtSigner()` / `createJwtVerifier()` with `alg`-discipline and mandatory `exp`; `jwk()` JWKS middleware (asymmetric-only); `bearerAuth({ verify })`; `basicAuth()` with UTF-8 credential decoding and construction-time validation; `passwordHash` / `passwordVerify` at `@daloyjs/core/hashing`; `session()` with `__Host-` cookie + HMAC-SHA256; `loginThrottle()`; `wsRateLimit()`; `rotateSession()`; `Cache-Control: no-store` baked into `UnauthorizedError` / `ForbiddenError` / `TooManyRequestsError`. Refuse-to-boot on weak session secrets and short HS-JWT keys. | Pick the right identity provider (see [Auth integrations](/docs/auth)) and rotate secrets/keys on a schedule. |
| **API3: Broken Object Property Level Authorization** (excessive data exposure + mass assignment). | Contract-first `app.route()` with `request` *and* `responses` schemas (Zod / Valibot / ArkType / TypeBox). Only fields you declare in the response schema are emitted. Undeclared fields a handler returns (a stray `passwordHash`, a spread ORM row) are stripped at serialization, not just flagged. Only fields you declare in the request schema reach your handler. Surfaced in OpenAPI so reviewers can audit every payload. | Author response schemas that omit internal fields. Don't spread raw ORM rows into responses. Stripping only runs when a `2xx` response declares a `body` schema, so `daloy doctor` emits `audit.response.bodySchema` (and a dev-mode boot warning) for any success response that has none. For routes that intentionally return an opaque or framework-controlled body (a raw `Response`, HTML, a proxied payload), set `acknowledgeNoResponseBodySchema: true` on the route to record that intent and silence the finding. Successful raw `preBody`/`beforeHandle` short-circuits require the same acknowledgement; error denials do not. Framework-mounted routes (`/openapi.json`, `/docs`, `/asyncapi`, health, metrics) acknowledge themselves. The warning only ever names routes you wrote. |
| **API4: Unrestricted Resource Consumption** | Body-size cap (default 1 MiB, `Content-Length` checked first → `413`); per-route `request.timeout`; `rateLimit()` with `groupId` shared buckets; `@daloyjs/core/rate-limit-redis` for multi-instance deploys; `loadShedding()`; `ipRestriction()` with CIDR-aware allow/deny; multipart per-field size caps and MIME allowlist; `compression()` with BREACH-aware skips and `minimumSize` + negative-ratio guard; connection-draining shutdown. | Pick numeric limits that match your traffic budget. Run a Redis-backed limiter when you have more than one process. |
| **API5: Broken Function Level Authorization (BFLA)** | `requireScopes()` with RFC-6750 challenge and per-request aggregation; per-route middleware via `combine` (`every` / `some` / `except`) so admin actions are explicit, not implicit; `internal: true` route flag (`404` via `app.fetch`, dispatched only via `app.inject`); namespace-protected decorators prevent accidental privilege bleed across plugins. | Define your scope/role catalog and apply `requireScopes()` to every admin or destructive route. |
| **API6: Unrestricted Access to Sensitive Business Flows** | `rateLimit({ groupId })` to share a bucket across related endpoints (checkout, refund, transfer); `loginThrottle()` for credential stuffing; `wsRateLimit()` for socket abuse; `csrf()` + Fetch-Metadata enforcement to refuse cross-origin state-changing requests by default. | Threat-model the abuse case (coupon stacking, repeated transfers, mass account creation) and group the limiter accordingly. |
| **API7: Server-Side Request Forgery (SSRF)** | `fetchGuard()` wraps the global `fetch` and refuses requests to loopback, RFC1918, link-local (including every documented cloud metadata IP: AWS / Azure / DigitalOcean `169.254.169.254`, Oracle `192.0.0.192`, Alibaba `100.100.100.200`), and IPv6 unique-local. Throws `SsrfBlockedError` with a reason code so handlers can surface a clean `422`. See [SSRF guard](/docs/security/fetch-guard). | Use the guarded fetch returned by `fetchGuard()` (e.g. `const safeFetch = fetchGuard()`) for *every* outbound call whose URL is influenced by user input. |
| **API8: Security Misconfiguration** | `secureHeaders()` auto-applied (CSP with nonce + Trusted Types, HSTS, frame-defense); `Server` and `X-Powered-By` stripped; duplicate `Host` / `Content-Length` rejected; `safeJsonParse` strips `__proto__` / `constructor` / `prototype`; header injection / response splitting guards; path-traversal rejection; per-route `accepts` content-type opt-in; cross-origin state-changing requests refused with `403` unless `cors()` allows; `cors()` `methods: ["*"]` refused at construction and default `allowMethods` narrowed to `[GET, HEAD, POST]`; CSP report receiver refuses non-`application/json` and bodies over 64 KiB; refuse-to-boot in production on `cors({ origin: "*"})`, weak session secrets, unconfigured `X-Forwarded-*`, missing `csrf()` alongside `session()` with state-changing routes, and unauthenticated health endpoints without explicit acknowledgement. Run `daloy doctor` and `daloy doctor --audit-defaults` in CI. See [Secure-by-default](/docs/security/secure-defaults) and [Boot guards](/docs/security/boot-guards). | Set `app({ behindProxy })` correctly for your deployment and keep TLS termination configured upstream. |
| **API9: Improper Inventory Management** | OpenAPI 3.1 generated from your routes (single source of truth, no annotations); `GET /openapi.json` + `GET /openapi.yaml`; `daloy inspect` for routes / dead routes / missing `operationId`; `daloy inspect --ai` dump for agents and audit tooling; typed client codegen via Hey API (`pnpm gen`) so downstream consumers track the same contract. | Decommission old API versions instead of leaving them mounted. Treat shadow endpoints (the ones no route file defines) as bugs. |
| **API10: Unsafe Consumption of APIs** | `fetchGuard()` for outbound calls (blocks SSRF pivots through third-party redirects); `verifyWebhookSignature()` / `signWebhookPayload()` from `@daloyjs/core` for HMAC-verified inbound webhooks (HMAC-SHA256 / 384 / 512, accepting GitHub-style `sha256=` prefixes or raw hex); Standard Schema validation on third-party response bodies so a compromised vendor can't inject unexpected fields; request timeouts so a slow upstream can't exhaust your event loop. | Pin the vendor URL list, set `fetchGuard()` options to match your egress policy, and validate every external payload like it were user input. |

## Cross-cutting best practices

The Aikido guide also calls out general defences that aren't in the Top 10 list but matter for any API. Here's where Daloy addresses each.

| Practice | Daloy |
| --- | --- |
| Encryption in transit | HSTS via `secureHeaders()`; `__Host-` / `__Secure-` cookies refused over plain HTTP; refuse-to-boot if behind a proxy you haven't declared. |
| Input validation and schema enforcement | Standard Schema (Zod 4 / Valibot / ArkType / TypeBox) on `request.params` / `query` / `headers` / `json` / `form` / `multipart`; rejected requests turn into RFC 9457 problem+json. |
| Output validation (no surprise fields) | `responses` schemas validated per status code; mismatched payloads fail loudly in dev and are stripped in prod. |
| Rate limiting and throttling | `rateLimit()`, `rateLimit({ groupId })`, `loginThrottle()`, `wsRateLimit()`, `@daloyjs/core/rate-limit-redis`. |
| Logging without leaks | Structured pluggable logger with default redaction of common secret keys (`authorization`, `password`, `token`, `cookie`,...); request-id propagation; `requestId()` trust-default audit. |
| Error handling without info leak | RFC 9457 problem+json with prod-mode redaction; stack traces never leave the process in production; `httpError({ res })` refuses state-mutating headers. |
| Health checks without DoS amplification | `app.healthcheck()` / `app.readinesscheck()` with optional bearer-token auth and per-IP rate limit; refuse-to-boot in production without explicit `acknowledgeUnauthenticated: true`. |
| Supply-chain hardening | `ignore-scripts=true`, `minimum-release-age=1440`, SHA-pinned actions, CodeQL + Opengrep, OpenSSF Scorecard, npm trusted publishing with provenance, SBOM. See [Supply-chain security](/docs/security/supply-chain). |
| Zero-trust posture between services | JWT / JWKS verification helpers, scheme-aware `ctx.state.auth` typed contract, namespace-protected decorators, plugin `dependencies: string[]` refuse-to-boot. |

## What no framework can do for you

- Business-logic abuse: only your code knows whether a sequence of valid API calls amounts to fraud. Threat-model the workflow.
- Object-level authorization: Daloy enforces *who* can call a route; *which* records they can touch is application logic.
- Data classification: deciding which fields are sensitive is a product decision. Daloy keeps unwanted fields out if you list them in the response schema.
- Penetration testing: automated scanners catch common issues; a human tester catches logic chains. Run both on a schedule.

## Verify your posture

- `daloy doctor`: deployment-config posture checks.
- `daloy doctor --audit-defaults`: live secure-by-default audit.
- `pnpm verify:parity-audits`: static gates that fail CI if a secure default is regressed.
- `pnpm verify:governance-audits`: release-workflow rotation and governance floor.
- `daloy inspect`: route inventory, dead routes, missing `operationId`s.

Report a vulnerability via GitHub's private advisory at [github.com/daloyjs/daloy/security/advisories/new](https://github.com/daloyjs/daloy/security/advisories/new).

---

Source: https://daloyjs.dev/docs/security/owasp-api-top-10