OWASP API Security Top 10 mapping
The OWASP API Security Top 10 (2023) 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 (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).
The mapping
| OWASP API risk | What Daloy gives you | Still your job |
|---|---|---|
API1 — Broken Object Level Authorization (BOLA). Attacker swaps an idin 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 resource.ownerId === auth.userId check inside the handler. No framework can know your ownership model. See Authentication. |
| 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) 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; only fields you declare in the request schema are accepted. Surfaced in OpenAPI so reviewers can audit every payload. | Author response schemas that omit internal fields. Don't spread raw ORM rows into responses. |
| API4 — Unrestricted Resource Consumption | Body-size cap (default 1 MiB, Content-Lengthchecked 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 andminimumSize + 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 fetchand 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. | Use safeFetch 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 and 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() at @daloyjs/core/hashing for HMAC-verified inbound webhooks (sha256=-prefixed only); 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. |
| 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, missingoperationIds.
Report a vulnerability via GitHub's private advisory at github.com/daloyjs/daloy/security/advisories/new.