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.
What the core enforces
These checks happen in App or the runtime adapter itself. Applications get them without calling any middleware.
| 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 | Router rejects .. segments and // before walking. |
| Slow-loris / hung handlers | requestTimeoutMs aborts handlers (default 30s); Node adapter sets 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.
import {
requestId,
secureHeaders,
cors,
rateLimit,
bearerAuth,
timing,
} from "@daloyjs/core";
app.use(requestId()); // x-request-id propagation
app.use(secureHeaders()); // CSP, HSTS, X-Frame-Options, COOP, CORP, no-sniff …
app.use(cors({ // explicit allowlist; never * with credentials
origin: ["https://app.example.com"],
credentials: true,
methods: ["GET", "POST"],
}));
app.use(rateLimit({ // global by default; add keyGenerator or trusted proxy headers for per-client limits
windowMs: 60_000,
max: 120,
}));
app.use(timing()); // Server-Timing header for observabilityThe official starters wire these in for you: Node, Bun, and Deno enablesecureHeaders(), requestId(), and rateLimit(); Cloudflare Worker and Vercel Edge enable secureHeaders() andrequestId() plus tighter edge-friendly body and timeout limits.
Recommended by deployment target
Start with the middleware below unless you have a concrete reason not to. The point is not to hide policy behind a boolean flag; it is to make the 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 Edge | 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 with the double-submit-cookie pattern. The middleware sets a token cookie on safe requests, requires the same value on the x-csrf-token header for unsafe methods, and rejects mismatches with a timing-safe 403.
import { csrf } from "@daloyjs/core";
app.use(csrf()); // __Host-daloy.csrf cookie + x-csrf-token header by defaultsecureHeaders() defaults
content-security-policy: default-src 'self'; frame-ancestors 'none'
strict-transport-security: max-age=31536000; includeSubDomains
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: no-referrer
permissions-policy: camera=(), microphone=(), geolocation=()
cross-origin-opener-policy: same-origin
cross-origin-resource-policy: same-origin
x-xss-protection: 0If you need a different CSP, want to disable HSTS in local development, or need a looser permissions policy, pass options to secureHeaders() explicitly.
Auth
import { bearerAuth, timingSafeEqual } from "@daloyjs/core";
app.route({
method: "POST",
path: "/admin/purge",
operationId: "adminPurge",
hooks: bearerAuth({
validate: (token) => timingSafeEqual(token, process.env.ADMIN_TOKEN!),
realm: "admin",
}),
responses: { 204: { description: "ok" }, 401: { description: "denied" } },
handler: async () => ({ status: 204 as const, body: undefined }),
});Supply-chain
DaloyJS is distributed via pnpm for a stricter install model, and the project's own defaults add hardened install and 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 — CI/CD actions are pinned to immutable commits, not mutable tags.
- Protected npm publishing — tag-only release workflow, protected environment approval, OIDC trusted publishing, and
--provenance.
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 behind a trusted proxy.
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.
import {
swaggerUiHtml,
htmlResponse,
} from "@daloyjs/core/docs";
const nonce = crypto.randomUUID();
const html = swaggerUiHtml({
specUrl: "/openapi.json",
scriptNonce: nonce,
assets: {
swaggerUiCssUrl: "/docs-assets/swagger-ui.css",
swaggerUiBundleUrl: "/docs-assets/swagger-ui.js",
},
});
return htmlResponse(html, {
assetOrigins: [],
scriptNonce: nonce,
allowInlineStyles: false,
});# .npmrc
ignore-scripts=true
minimum-release-age=1440
strict-peer-dependencies=true
prefer-frozen-lockfile=true
verify-store-integrity=true
provenance=trueFor the full CI/CD and maintainer playbook, read Supply-chain security. Run pnpm audit --prod in CI and before release.
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.