Security
Bad defaults are bugs. DaloyJS separates core-enforced guardrails from first-party security middleware so the dangerous things are blocked by default and the deployment-specific things stay explicit.
Core guardrails protect every app by default. First-party middleware adds the policy that depends on your routes, users, and deployment.
What the core enforces
These checks happen in App or the runtime adapter itself. Applications get them without calling any middleware.
- 01ingressIncoming requestmethod, path, headers, body
- 02size capBody-size limitContent-Length over cap to 413
- 03parseHardened JSON parsestrips __proto__ / constructor
- 04routeRouter + method check.. resolved to canonical path; bad method 405
- 05handlerYour typed handlerruns under requestTimeoutMs
| Threat | Built-in behavior |
|---|---|
| Body-size DoS | Streamed read, hard cap (default 1 MiB), Content-Length checked first -> 413. |
| Prototype pollution | safeJsonParse strips __proto__, constructor, prototype via reviver. |
| Header / response splitting | sanitizeHeaderName / sanitizeHeaderValue reject CRLF + NUL. |
| Path traversal | Dot-segments (. / ..) are resolved to a canonical path before route matching, and empty // segments are refused. Routes match exact strings, so there is no directory to escape into. |
| Slow-loris / hung handlers | requestTimeoutMs (default 30s) returns 408 and fires ctx.request.signal for cooperative teardown; the Node adapter also sets socket timeouts. |
| Unsupported content types | Routes with body schemas reject non-allowed content-types -> 415. |
| Method confusion | Real 405 with Allow header, never a misleading 404. |
| Information disclosure (5xx) | Production mode strips detail from 5xx problem+json automatically. |
First-party security middleware
These are part of DaloyJS and documented together, but they stay explicit because CSP, CORS, rate-limit keys, session secrets, and CSRF rollout are deployment decisions.
Put rateLimit() or loginThrottle() before bearer/basic/JWK/mTLS auth when it must count failed credentials. The limiter will still spend the attempt and return 429 after the cap even though auth rejects in preBody; DaloyJS does not read a declared request body to do it. A custom key generator on that path should use raw request data or state populated by an earlier preBody hook.
The official starters wire these in for you: Node, Bun, and Deno enable secureHeaders(), requestId(), and rateLimit(); Cloudflare Worker and Vercel enable secureHeaders() and requestId() plus tighter edge-friendly body and timeout limits.
Recommended by deployment target
Start with the middleware below unless you have a concrete reason to change it. This keeps risky choices explicit and consistent.
| Target | Recommended baseline |
|---|---|
| Node / Bun / Deno API | requestId(), secureHeaders(), rateLimit(), and cors() when the API is cross-origin. |
| Cloudflare Workers | requestId() and secureHeaders() by default; use cors() only when needed, and prefer an external/shared limiter over the in-memory default when traffic spans many isolates. |
| Vercel | requestId() and secureHeaders() by default; add cors() only when needed, and use a shared limiter if you need durable counters across regions. |
| Cookie-authenticated app | Add session() plus csrf() on top of the baseline so mutating routes are protected against cross-site form and fetch attacks. |
| Behind a trusted reverse proxy | Keep the baseline, then configure rateLimit() with an explicit keyGenerator or set trustProxyHeaders: true only after the proxy strips and rewrites forwarding headers. |
csrf() for state-changing routes
Use csrf() to protect mutating endpoints. Two strategies are supported:
- Double-submit cookie (default): sets a token cookie on safe requests, requires the same value on the
x-csrf-tokenheader for unsafe methods, and rejects mismatches with a timing-safe 403. - Fetch Metadata (
strategy: "fetch-metadata") - tokenless protection that relies on the modernSec-Fetch-Siteheader. No cookie round-trip; no HTML rendering coupling. Recommended for new browser-facing apps.
secureHeaders() defaults
If you need a different CSP, want to disable HSTS in local development, or need a looser permissions policy, pass options to secureHeaders() explicitly. The legacy X-XSS-Protection: 0 header is opt-in via xssProtection: true for deployments that want to explicitly disable old browser XSS filters.
CSP with per-request nonces & Trusted Types
secureHeaders() can build the CSP from a directive map and inject a fresh per-request nonce into script-src, script-src-elem, style-src, and style-src-elem, plus emit require-trusted-types-for 'script' for runtime DOM XSS hardening. The nonce is exposed at ctx.state.cspNonce so handlers can render it into <script nonce="..."> tags.
Do not render this page with htmlResponse() from @daloyjs/core/docs: that helper ships its own Content-Security-Policy (tuned for the Swagger / Scalar docs UIs, with 'unsafe-inline') and would override the strict nonce CSP above, so the nonce would no longer be the thing gating inline scripts. Keep htmlResponse() for your API-docs route, and return your own Response body for nonce-protected pages.
Auth
SQL injection
Daloy doesn't ship a database driver, but the HTTP boundary it does own (strict Zod schemas, hardened JSON parser, body-size caps) shrinks the surface that reaches your repository layer. See SQL injection for the safe vs. unsafe patterns per ORM (Prisma, Drizzle, Kysely, raw drivers), an allowlisting recipe for dynamic ORDER BY, and the grep rules the maintainers use to catch regressions.
Command injection
DaloyJS's runtime is child_process-free by CI gate, so the framework itself cannot shell out. See Command injection for the safe shape of a handler that does need to invoke an external program (execFile + argv array, never exec(`cmd $${input}`)), the Windows BatBadBut footgun, and the grep rules to keep new bugs out at PR time.
Admin panels
Building an admin or customer-success surface on top of DaloyJS? See Secure admin panels for the recommended pattern: internal: true routes, ipRestriction(), strict CSP with per-request nonces, per-admin authentication, login-throttle rateLimit() groups, and structured audit logging, mapped one-to-one to Aikido's public "secure admin panel" checklist.
Supply-chain
DaloyJS is distributed via pnpm for a stricter install model. Scaffolded pnpm apps inherit the install-time controls, while DaloyJS's own repository and the optional GitHub Actions bundle add CI/CD controls against the cache-poisoning, maintainer-phishing, and OIDC token-abuse patterns seen in recent npm incidents.
- Strict isolation: packages cannot reach phantom dependencies.
- Content-addressable store: every byte is hashed and verified.
- Frozen lockfile in CI with
--ignore-scripts: reproducible installs without transitive lifecycle execution. verify-store-integrity, corruption-detecting reads.strict-peer-dependencies, no silent peer mismatches.minimum-release-age=1440, wait 24h before installing fresh releases.ignore-scripts=truewith explicitpnpm.onlyBuiltDependencies: reviewed allowlist for native install scripts.- SHA-pinned GitHub Actions: the optional generated GitHub workflows pin third-party actions to immutable commits, not mutable tags.
- Protected DaloyJS npm publishing: the framework's own packages use a tag-only release workflow, protected environment approval, OIDC trusted publishing, and
--provenance.
If your generated app lives outside GitHub, carry over the portable parts directly and translate the GitHub workflow rules to your CI host. The framework cannot enforce branch protection or runner egress in a private GitLab, Bitbucket, Azure DevOps, or on-prem installation.
Trusted proxies and rate limiting
DaloyJS no longer trusts X-Forwarded-For or X-Real-IP by default when deriving a rate-limit key. Those headers are client-spoofable unless your reverse proxy strips and rewrites them. The default limiter is therefore global until you provide an explicit keyGenerator or opt in to trustProxyHeaders: true / trustedProxies behind a trusted proxy. When proxy headers are trusted, the key is the rightmost X-Forwarded-For entry (the one your proxy appended), never an attacker-prepended left entry; multi-hop chains declare their hop count with trustedHops. When the origin itself can be reached, set trustedProxies so the peer socket is verified against a CIDR allowlist before any forwarded header is believed (see the autoBan note).
For credential-entry routes, use loginThrottle() across /login, OTP, and password-reset routes, and wsRateLimit() on related WebSocket upgrades. Both helpers can spend from the same groupId bucket.
Self-hosted docs assets
The built-in docs helpers no longer force a jsDelivr-shaped CSP. You can self-host the Swagger UI or Scalar assets, add a nonce to the bootstrap script, and emit a same-origin CSP for your docs route.
For the full CI/CD and maintainer playbook, read Supply-chain security. Run pnpm audit --prod in CI and before release.
OWASP API Security Top 10 mapping
For a per-item walkthrough of how Daloy addresses every entry in the OWASP API Security Top 10 (2023) plus the cross-cutting best practices (encryption, validation, rate limiting, logging, inventory, third-party API safety), read OWASP API Top 10 mapping.
Reporting a vulnerability
Use GitHub's private vulnerability reporting at github.com/daloyjs/daloy/security/advisories/new with reproduction steps. Do not open a public issue with exploit details.