Search docs

Jump between documentation pages.

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.

ThreatBuilt-in behavior
Body-size DoSStreamed read, hard cap (default 1 MiB), Content-Length checked first → 413.
Prototype pollutionsafeJsonParse strips __proto__, constructor, prototype via reviver.
Header / response splittingsanitizeHeaderName / sanitizeHeaderValue reject CRLF + NUL.
Path traversalRouter rejects .. segments and // before walking.
Slow-loris / hung handlersrequestTimeoutMs aborts handlers (default 30s); Node adapter sets timeouts.
Unsupported content typesRoutes with body schemas reject non-allowed content-types → 415.
Method confusionReal 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.

ts
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 observability

The 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.

TargetRecommended baseline
Node / Bun / Deno APIrequestId(), secureHeaders(), rateLimit(), and cors() when the API is cross-origin.
Cloudflare WorkersrequestId() 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 EdgerequestId() and secureHeaders() by default; add cors() only when needed, and use a shared limiter if you need durable counters across regions.
Cookie-authenticated appAdd session() plus csrf() on top of the baseline so mutating routes are protected against cross-site form and fetch attacks.
Behind a trusted reverse proxyKeep 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.

ts
import { csrf } from "@daloyjs/core";

app.use(csrf()); // __Host-daloy.csrf cookie + x-csrf-token header by default

secureHeaders() defaults

text
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: 0

If you need a different CSP, want to disable HSTS in local development, or need a looser permissions policy, pass options to secureHeaders() explicitly.

Auth

ts
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=true with explicit pnpm.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.

ts
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,
});
ini
# .npmrc
ignore-scripts=true
minimum-release-age=1440
strict-peer-dependencies=true
prefer-frozen-lockfile=true
verify-store-integrity=true
provenance=true

For 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.