Runtime protections
Runtime protections ship with the application and behave the same way on every adapter. CI controls protect the build and release path. These controls protect requests after deployment.
These protections live inside @daloyjs/core and run at request time in your app process. They apply regardless of where you host the repo or which CI you use: private GitHub, GitLab, Bitbucket, Azure DevOps, Gitea, self-managed Jenkins, or on-prem runners. They are also unaffected by whether you keep or delete the optional GitHub Actions bundle from create-daloy.
DaloyJS splits supply-chain and security posture into three independent layers. This page documents the first one.
The three layers, side by side
| Layer | Where it runs | Travels to GitLab / Bitbucket / Azure / on-prem? |
|---|---|---|
| Runtime guardrails (this page) | Inside your app, every request. Lives in @daloyjs/core. | Yes. Always on. No CI host required. |
| Install-time hardening | pnpm-workspace.yaml in pnpm scaffolds (pnpm 11 reads it; the .npmrc mirror is for npm and older tooling). From 1.5.4, npm scaffolds get ignore-scripts=true and a release-age setting in .npmrc, Yarn scaffolds a .yarnrc.yml with enableScripts: false and a minimum release age, and Bun scaffolds a bunfig.toml release age. pnpm verify:lockfile comes with the --with-ci bundle. | Yes. The settings ship in the project itself. |
| CI / CD hardening | .github/workflows/*.yml from create-daloy --with-ci. | GitHub only (public or private repo / org). On other CIs you have to translate the rules yourself. |
Backend footguns handled by default
Every row below is either on by default in a fresh DaloyJS app or applies automatically whenever you use the named first-party helper. You do not need to install a plugin or deploy on a specific CI to get these. Rows marked production are boot refusals that apply only when the environment resolves to production (env: "production", production: true, or NODE_ENV=production); with no signal, or an unrecognized NODE_ENV from 1.5.4, they are logged as warnings instead. These rows cover known footguns; they are not a guarantee that an app is secure.
| Footgun | What DaloyJS does |
|---|---|
| Unsafe CORS defaults | Cross-origin state-changing requests are refused unless cors() explicitly allows the origin. cors() throws for origin: "*" with credentials: true in every environment, and from 1.5.4 also for credentials: true with an origin predicate or list that allows every origin or "null". |
| Missing CSRF on stateful routes | Production: booting session() with mutating routes but without csrf() is refused at startup, not silently allowed. |
| Weak session secrets | Production: a session secret shorter than 32 bytes, matching a known placeholder, or made of a single repeated character is refused at boot. |
| Missing secure response headers | secureHeaders() is auto-applied: HSTS, frame deny, no-sniff, strict referrer policy, baseline CSP. |
| Prototype pollution via JSON bodies | safeJsonParse strips __proto__, constructor, and prototype keys before the value reaches your handler. |
| 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. |
| Body-size abuse | Streamed reads with a hard cap (bodyLimitBytes, default 1 MiB); oversize requests return 413. From 1.5.4 the cap applies to every route, including handlers that read ctx.request directly (text(), json(), arrayBuffer(), formData(), the body stream, clone()). Before 1.5.4 only routes with a request-body schema were capped. |
| Hung handlers / slow-loris | requestTimeoutMs (default 30s) returns 408 and fires ctx.request.signal for cooperative teardown. From 1.5.4 the timeout covers the preBody, beforeHandle and afterHandle hooks, the request-body read, and the handler, measured from the first asynchronous step; before 1.5.4 it covered the handler only. The Node adapter sets socket-level timeouts. |
| Limits disabled by a bad value | From 1.5.4, bodyLimitBytes, requestTimeoutMs, maxHeaderCount, jsonMaxKeys and jsonMaxDepth must be finite non-negative integers, checked at construction in every environment. NaN (for example Number(process.env.UNSET)), Infinity, negatives, non-integers and strings throw instead of silently disabling the limit; an explicit undefined uses the default. rateLimit() likewise refuses a windowMs that is not a positive integer or a max that is not a non-negative integer (max: 0 still refuses every request). |
| Header-count flood / HTTP/2 Bomb | maxHeaderCount (default 100) is a portable application-tier guard that returns 431 Request Header Fields Too Large when a request exceeds the cap (e.g. via app.fetch() or runtimes without a native cap). The Node adapter also sets the native server.maxHeadersCount to the same value. Because Node's llhttp parser silently truncates headers past that cap instead of rejecting, the adapter additionally answers 431 for any HTTP request or WebSocket upgrade whose raw field count reaches the cap (usable default budget: 99 fields). Either way it is defence-in-depth. Apply the vendor HTTP/2 fix at any proxy that terminates HTTP/2. |
| DNS rebinding | Set allowedHosts and any request whose Host is not listed gets 400 before routing, which stops a rebound page from passing the cross-origin check. serve() defaults to localhost, *.localhost and IP literals in development. See boot guards. |
| Bad reverse-proxy assumptions | X-Forwarded-* headers are never trusted by default. In production, a request carrying one while trustProxy / behindProxy is unconfigured is refused with a 500 (and a clear log line) so a spoofed source IP can't reach the rate limiter or audit log. Dev and CI relax this so you can test forwarded headers locally. Opt in via behindProxy (or trustProxy: false to ignore the headers). From 1.5.4, behindProxy: { cidrs } and the guards' trustedProxies refuse a /0 range (0.0.0.0/0, ::/0), which would trust every peer. |
| Auth response caching | 401, 403, and 429 automatically set Cache-Control: no-store so proxies and CDNs cannot reuse them. |
| Duplicate dangerous headers | Duplicate Host and Content-Length are rejected at parse time to block request-smuggling shapes. |
| Weak JWT secrets | createJwtSigner() refuses HS* secrets shorter than the algorithm requires. |
| Missing JWT expiry | Signing without an exp claim is refused, not defaulted to a forever token. |
| Missing JWT audience | Production, 1.5.4: jwk() and createJwtVerifier() refuse to construct without an audience, unless allowAnyAudience: true is passed for a single-tenant private identity provider. |
| Unsafe compression cases (BREACH) | compression() skips Set-Cookie, Authorization, session / CSRF cookie responses, and already-encoded content; downgrades strong ETags per RFC 9110. |
| Unsafe file-upload assumptions | multipartObject + fileField enforce per-field size caps, MIME allowlists, and magic-byte checks. |
| Leaky production errors | 5xx problem+json detail is redacted unless the environment is explicitly development or test; an unset NODE_ENV redacts too. Stack traces never leak through the default error path. |
| Unsupported content types | Routes with body schemas reject non-allowed content-types with 415. |
| Method confusion | Real 405 with Allow header instead of a misleading 404. |
| Header / response splitting | sanitizeHeaderName / sanitizeHeaderValue reject CRLF and NUL in header values. |
What this page does not cover
The following protections only apply if you keep using the matching scaffolded bits:
- Install-time hardening (blocked install scripts, 24h release-age cooldown) applies when you keep the scaffold's package-manager settings (
pnpm-workspace.yamlfor pnpm). Source-verified lockfiles (pnpm verify:lockfile) come with the--with-cibundle. Blocked install scripts do not stop a payload that runs when a package is imported; the cooldown and your lockfile are the mitigation there. - CI / CD hardening (pinned actions,
harden-runner, top-levelpermissions: {}, CODEOWNERS, Dependabot, CodeQL / Scorecard / zizmor) applies when you use thecreate-daloy --with-ciGitHub Actions bundle. On GitLab, Bitbucket, Azure DevOps, Jenkins, or on-prem runners you have to translate those rules into your CI's own configuration. - Branch protection, environment approvals, secret hygiene, runner isolation, and org policy are decisions of the host (GitHub / GitLab / Azure / Bitbucket / your own infra). DaloyJS cannot enforce them from inside your code.
What the generated GitHub Actions bundle does
If you scaffold with create-daloy --with-ci and keep the generated workflows, the YAML itself encodes these protections. They apply equally to public repos, private repos, and private organizations. Being private is not a substitute for any of them.
- Top-level
permissions: {}with least-privilege per-job permissions. - Third-party Actions pinned to a commit SHA (not a moving tag).
actions/checkoutwithpersist-credentials: false.step-security/harden-runnerwith egress policy on every job.- Lifecycle scripts disabled during CI installs (
--ignore-scriptsfor npm/yarn,ignoreScripts: truein the scaffoldedpnpm-workspace.yamlfor pnpm). - No shared Actions cache by default.
- Dependabot config for npm + Actions ecosystems.
CODEOWNERSfor security-sensitive files.- CodeQL, OpenSSF Scorecard, zizmor, and vulnerability-scan workflows where included.
- Manual-only
deploy.ymlstarter instead of automatic publish or deploy on push.
This is generated GitHub CI hardening, not “default supply-chain protection everywhere”. If you delete the workflows, rewrite them, or use a different CI host, DaloyJS cannot provide these protections automatically.
Protection matrix
Use this table to figure out which protections you actually get for a given setup.
| User setup | What DaloyJS can protect |
|---|---|
create-daloy --with-ci on GitHub (private or public repo / org) | Runtime guardrails + pnpm install-time hardening (if pnpm) + full generated GitHub Actions starter protections. |
create-daloy with pnpm, --no-ci | Runtime guardrails + hardened install defaults via pnpm-workspace.yaml (no pnpm verify:lockfile script). |
| npm / yarn / bun scaffolds | Runtime guardrails. From 1.5.4, npm and Yarn scaffolds also block lifecycle scripts and set a release-age cooldown; Bun scaffolds set a release-age cooldown. CI install commands still benefit if you keep the generated workflows on GitHub. |
| GitLab / Bitbucket / Azure DevOps / Jenkins / on-prem | Runtime guardrails and portable docs / patterns. No GitHub Actions protections. Translate the YAML rules into your CI's own configuration. |
| User deletes or rewrites the generated workflows | Runtime guardrails only. DaloyJS cannot vouch for the CI supply-chain posture once the workflows are gone. |
| Branch protection, environment approvals, secret hygiene, runner isolation, egress policy, org settings, deploy-platform config | Out of scope. These are decisions of your repo host and deploy platform. DaloyJS cannot enforce them from inside your code. |
See Secure-by-default, Boot guards, and Supply-chain security for the full surface of each layer.