Security best practices
Este conteúdo não está disponível em sua língua ainda.
The framework ships the pieces; this page is how to assemble them. The
runnable counterpart is examples/http/secure-service.ts.
The recommended stack
Section titled “The recommended stack”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), )))))));| Layer | Why here |
|---|---|
requestId | outermost, so every log line (including errors) has an id |
securityHeaders | stamp headers on every response, even short-circuits |
cors | outside auth — preflights are anonymous by spec |
requestTimeout | bound latency before doing real work |
csrf | browser apps only; after CORS, before handlers |
handleErrors | map thrown errors to responses close to the routes |
fallback | at the root — answers anything unmatched |
What every response already carries
Section titled “What every response already carries”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.
Operational environment
Section titled “Operational environment”- 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.remoteAddressis the socket peer, not a spoofableX-Forwarded-For. Trust a forwarded header only when your proxy overwrites it (IpAllowlist’sgetClientIpoption). - Secrets from the environment, not HOCON files — the CSRF secret, Basic credentials, and bearer tokens all take values you load at startup.
Cookies
Section titled “Cookies”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.
Error hygiene
Section titled “Error hygiene”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.
Redirects
Section titled “Redirects”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.
Capping untrusted input
Section titled “Capping untrusted input”- 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
htmltemplate; to accept rich markup, sanitise with a dedicated library beforerawHtml.
Static files
Section titled “Static files”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.
WebSocket
Section titled “WebSocket”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.
Checklist
Section titled “Checklist”| Measure | How |
|---|---|
| MIME-sniffing disabled on every response | nothing — on by default |
| Security headers on every response | securityHeaders(), or withSecurityHeaders(...) server-wide |
| HTTPS enforced by the browser | strictTransportSecurity() / withHsts |
| Cross-origin controlled | cors(...) |
| CSRF (browser forms) | csrfProtection(...) |
| No internal error leaks | withErrorHandler + generic bodies |
| No open redirects | redirect(...) — same-origin by default |
| Request correlation | requestId() |
| Bounded latency | requestTimeout(...) |
| Safe file serving | getFromDirectory defaults (dotfiles deny, symlink confinement) |
