Aller au contenu
Français

Security best practices

Ce contenu n’est pas encore disponible dans votre langue.

The framework ships the pieces; this page is how to assemble them. The runnable counterpart is examples/http/secure-service.ts.

Order matters — outermost first:

import {
concat, cors, csrfProtection, fallback, handleErrors, path,
requestId, requestTimeout, securityHeaders, withMiddleware,
CorsOptions, CsrfOptions,
} from 'actor-ts/http';
const corsOptions = CorsOptions.create().withOrigins('https://app.example').withCredentials();
const csrf = csrfProtection(CsrfOptions.create().withSecret(process.env.CSRF_SECRET!));
const routes =
withMiddleware(requestId(),
withMiddleware(securityHeaders(),
cors(corsOptions,
withMiddleware(requestTimeout(15_000),
withMiddleware(csrf,
handleErrors(errorMapper,
concat(
appRoutes,
fallback(notFound),
)))))));
LayerWhy here
requestIdoutermost, so every log line (including errors) has an id
securityHeadersstamp headers on every response, even short-circuits
corsoutside auth — preflights are anonymous by spec
requestTimeoutbound latency before doing real work
csrfbrowser apps only; after CORS, before handlers
handleErrorsmap thrown errors to responses close to the routes
fallbackat the root — answers anything unmatched

X-Content-Type-Options: nosniff needs no setup. The backend writes it, not a middleware, so it also reaches the responses no middleware sees: the backend’s own error mapping, the fallback 404 and the body-too-large 413. A handler’s own header still wins.

It is the only one on by default, deliberately — it is the only header of the bundle that cannot change how an existing application is embedded, framed or referred to. X-Frame-Options and Cross-Origin-Resource-Policy would break iframes, cross-origin embedding and OAuth popups, so they stay opt-in.

To put the whole bundle on every response — including those same middleware-invisible paths — configure it on the server rather than the route tree:

import { SecurityHeadersOptions } from 'actor-ts/http';
const securityHeadersOptions = SecurityHeadersOptions.create()
.withFrameOptions('SAMEORIGIN')
.withReferrerPolicy('strict-origin-when-cross-origin');
await http.newServerAt('0.0.0.0', 8080)
.withSecurityHeaders(securityHeadersOptions)
.bind(routes);

Passing options opts into the full bundle, its own defaults included — so the two headers above arrive alongside Cross-Origin-Resource-Policy: same-origin and the rest. A plain object works as well (withSecurityHeaders({ frameOptions: 'SAMEORIGIN' })), and withSecurityHeaders(false) turns the mechanism off entirely.

Server-wide or as securityHeaders() middleware — both stamp the same set. Reach for the middleware when only a route subtree needs it; reach for the builder when the whole server does, because the middleware is skipped wherever a response never comes back through it.

  • TLS terminates at a proxy. Set HSTS regardless — browsers ignore it over plain HTTP. Don’t gate security headers on a scheme the app can’t see.
  • Trusting client IPs. req.remoteAddress is the socket peer, not a spoofable X-Forwarded-For. Trust a forwarded header only when your proxy overwrites it (IpAllowlist’s getClientIp option).
  • Secrets from the environment, not HOCON files — the CSRF secret, Basic credentials, and bearer tokens all take values you load at startup.

Prefer the __Host- prefix (requires Secure, Path=/, no Domain) and SameSite=Lax or Strict. The CSRF cookie is deliberately not HttpOnly (double-submit needs JS to read it); your session/auth cookie should be HttpOnly. serializeCookie refuses values that could inject a second header.

Don’t leak internals. Use withErrorHandler (or a top-level handleErrors) to return a generic body, and log the detail server-side keyed by the request id. Keep 403/404 responses unspecific — the static-file layer returns a uniform 404 for every rejected path so it never reveals what exists.

redirect(target) takes same-origin targets only — a relative reference. An absolute URL, a protocol-relative //host, or a control character throws HttpError(400), so forwarding a ?next= parameter straight into a redirect fails closed instead of becoming an open redirect. When leaving the origin is the intent, say so: redirectExternal(...) is the same helper without the origin rule, and being a separate name makes every deliberate off-origin hop greppable in one command. Hand it a constant or a target you allowlisted — never a raw request parameter. See the Route DSL.

  • Request bodies are capped by the backend (Fastify 1 MiB default; Express/Hono ~10 MiB) — tune per backend for your payloads.
  • WebSocket frames are capped by maxFrameBytes (1 MiB default).
  • Untrusted HTML — escape with the html template; to accept rich markup, sanitise with a dedicated library before rawHtml.

Keep the dotfiles: 'deny' and symlinks: 'within-root' defaults. Enable browse only for internal tools — a public listing exposes your directory structure. See Static files.

Wrap a websocket() route in cors(...) to reject cross-origin upgrades, and put auth middleware around it — withMiddleware runs at upgrade time, so it gates the handshake.

MeasureHow
MIME-sniffing disabled on every responsenothing — on by default
Security headers on every responsesecurityHeaders(), or withSecurityHeaders(...) server-wide
HTTPS enforced by the browserstrictTransportSecurity() / withHsts
Cross-origin controlledcors(...)
CSRF (browser forms)csrfProtection(...)
No internal error leakswithErrorHandler + generic bodies
No open redirectsredirect(...) — same-origin by default
Request correlationrequestId()
Bounded latencyrequestTimeout(...)
Safe file servinggetFromDirectory defaults (dotfiles deny, symlink confinement)