inkan

Secure headers

A strict Content-Security-Policy with nonces, HSTS, and the rest.

Needs inkan 0.8

@inkanjs/secure-headers needs inkan 0.8.0 or newer, below 1.0.

npm i @inkanjs/secure-headers
import {  } from "@inkanjs/secure-headers";


  .(())
  .("/", () => `<script nonce="${.cspNonce}">start()</script>`);

What helmet does for Express. Every header can be changed or left out, and a header the handler set itself is never overwritten.

What every answer gets

headerdefaultoption
content-security-policysee belowcsp
strict-transport-securitymax-age=31536000; includeSubDomains, over HTTPS onlyhsts
x-content-type-optionsnosniffnoSniff
x-frame-optionsDENYframeOptions
referrer-policyno-referrerreferrerPolicy
cross-origin-opener-policysame-origincrossOriginOpenerPolicy
cross-origin-resource-policysame-origincrossOriginResourcePolicy
x-dns-prefetch-controloffdnsPrefetchControl
permissions-policycamera, geolocation, microphone, payment, usb, interest-cohort offpermissionsPolicy
origin-agent-cluster?1originAgentCluster
x-permitted-cross-domain-policiesnonepermittedCrossDomainPolicies

A string sends another value, false leaves the header out. extra adds headers of your own the same way.

The default policy, exported as DEFAULT_CSP:

default-src 'self'; base-uri 'self'; font-src 'self' https: data:; form-action 'self';
frame-ancestors 'none'; img-src 'self' data:; object-src 'none'; script-src 'self';
script-src-attr 'none'; style-src 'self'

No 'unsafe-inline' anywhere: inline scripts and styles need a nonce.

secureHeaders({
  : {
    : { "img-src": ["'self'", "https://cdn.example.com"], "script-src-attr": false, "upgrade-insecure-requests": true },
    // useDefaults: false   start from nothing
    // reportOnly: true     content-security-policy-report-only
  },
  : { : 63072000, : true },
  : "cross-origin", // a public API that pages on other sites load
});

Behaviour

  • The nonce. ctx.cspNonce is 128 random bits, fresh for every request, made only when read. An answer whose handler (or template) read it gets 'nonce-…' in script-src and style-src; every other answer gets the plain policy.
  • HSTS goes out only over HTTPS: the socket is a TLS socket, or https(ctx) says so. Behind a proxy that ends TLS, pass https: (ctx) => ctx.headers["x-forwarded-proto"] === "https" (only when the proxy sets that header itself) or hsts: { force: true }.
  • Your headers win. A header the handler or an earlier hook set stays as it is, so one route can send its own policy.
  • inkan's own pages keep working. /docs, /openapi.json and the inspector are answered before any hook runs, so they never get these headers.
  • Problems (4xx, 5xx) get the headers too, and so do streams.

Options

optiondefault
csp{}false for none; directives over the defaults (false drops one, true for one without sources), useDefaults, reportOnly
hsts{}false for none; maxAge, includeSubDomains (true), preload (false), force (false)
httpsthe socket is a TLS socket(ctx) => boolean: whether the request came over HTTPS
skipnone(ctx) => boolean: answers to leave alone
extranonemore headers, by name
each header abovea string, or false

OpenAPI

Unchanged. It is a shared plugin: registered on the app it serves every route, inside a plugin only that plugin's routes.

On this page