Search docs

Jump between documentation pages.

Boot guards

Daloy ships the boot-guards slice of the secure-by-default initiative: four refuse-to-boot / first-request guards that turn the most common production misconfigurations into loud failures during startup instead of silent vulnerabilities under load.

All four guards are gated on the resolved environment being production (sources: app({ env: "production" }), then app({ production: true }), then NODE_ENV === "production") so dev and CI workflows keep working with sample secrets and ad-hoc headers. The single master escape hatch app({ secureDefaults: false }) disables every boot guard at once.

1. Weak session secret refuse-to-boot

app.use(session({ secret })) now refuses to register in production when the secret is shorter than 32 UTF-8 bytes, matches a well-known placeholder ("changeme", "your-jwt-secret", "it-is-very-secret", …), or is a single repeated character ("a".repeat(64), "0".repeat(64)). The check runs synchronously inside app.use(...) so the process exits during startup, not on first request.

ts
import { App, session } from "@daloyjs/core";

const app = new App({ env: "production" });

// Throws at boot — secret is >= 16 chars, but < 32 bytes.
app.use(session({ secret: "sixteen-chars-ok" }));

// Also throws — known weak placeholder.
app.use(session({ secret: "your-session-secret-for-production" }));

// Generate one with: openssl rand -base64 48
app.use(session({ secret: process.env.SESSION_SECRET! }));

Third-party session implementations can opt into the same check by stamping SESSION_HOOK_MARKER and SESSION_SECRETS_MARKER on the returned Hooks object. The standalone helper assertStrongSecret(secret, scope) is also exported for use in your own boot code.

2. cors({ origin: "*" }) refuse-to-boot

A wildcard CORS origin exposes every state-changing route cross-origin and is almost never what production wants. Daloy now refuses to register a cors() hook whose origin is "*" or an array containing "*" in production.

ts
import { App, cors } from "@daloyjs/core";

const app = new App({ env: "production" });

// Throws at boot.
app.use(cors({ origin: "*" }));

// Use an explicit allowlist instead.
app.use(cors({ origin: ["https://app.example.com"] }));

// Or a predicate.
app.use(cors({ origin: (o) => o.endsWith(".example.com") }));

3. session() + state-changing route without csrf()

When any route accepts POST, PUT, PATCH, or DELETE AND a session() hook is installed, a csrf() hook must also be installed. The check runs on first request (because route registration order is unknown until then) and the boot error is cached so every subsequent request rethrows the same failure until you fix the wiring.

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

const app = new App({ env: "production" });
app.use(session({ secret: process.env.SESSION_SECRET! }));
app.use(csrf({ strategy: "fetch-metadata", allowedOrigins: ["https://app.example.com"] }));

app.route({
  method: "POST",
  path: "/items",
  // ...
});

Non-browser apps (machine-to-machine APIs, webhook receivers behind bearer auth) can acknowledge that CSRF does not apply with app({ csrf: "off" }):

ts
const app = new App({ env: "production", csrf: "off" });
app.use(session({ secret: process.env.SESSION_SECRET! }));
// state-changing routes ok without csrf()

4. X-Forwarded-* with trustProxy unset returns 500

When app({ trustProxy }) is not set and a request arrives carrying X-Forwarded-For, X-Forwarded-Host, X-Forwarded-Proto, X-Forwarded-Port, or X-Real-IP, Daloy refuses to dispatch the request and returns a structured 500 problem+json. The rate limiter, audit log, and request-id propagation would otherwise honour the attacker-supplied IP.

ts
// Pick exactly one in production:

// (a) Running behind a trusted reverse proxy (nginx, ALB, Cloudflare):
const app = new App({ env: "production", trustProxy: true });

// (b) Direct-to-process — ignore forwarded headers:
const app = new App({ env: "production", trustProxy: false });

// (c) Disable every boot guard (escape hatch):
const app = new App({ env: "production", secureDefaults: false });

The warning is logged at warn exactly once per process via a latch, so a flood of forged requests does not flood your logs.

Migration checklist

  • Audit every session({ secret }) call — regenerate any secret shorter than 32 bytes with openssl rand -base64 48.
  • Replace cors({ origin: "*" }) with an explicit allowlist or predicate.
  • Add app.use(csrf(...)) next to app.use(session(...)), or pass app({ csrf: "off" }) for non-browser-facing apps.
  • Pick a trustProxy posture explicitly for every production app.