@blixis-io/security
Optional production middleware for @blixis-io/http: CORS, security headers, rate limiting and a proxy-aware client address. See Securing the API for how to use it. Peer dependency: @blixis-io/http.
function cors(options: CorsOptions): Middleware;
interface CorsOptions { origins: readonly string[] | "*" | ((origin: string) => boolean); methods?: readonly string[]; // default GET, HEAD, PUT, PATCH, POST, DELETE allowedHeaders?: readonly string[]; // default: what the preflight asks for exposedHeaders?: readonly string[]; credentials?: boolean; // default false; never with "*" maxAge?: number; // seconds, default 600}Throws SecurityConfigError when created for credentials with "*", an origin that is not exact, or a bad maxAge.
securityHeaders
Section titled “securityHeaders”function securityHeaders(options?: SecurityHeadersOptions): Middleware;
interface SecurityHeadersOptions { contentTypeOptions?: string | false; // default "nosniff" referrerPolicy?: string | false; // default "no-referrer" frameOptions?: string | false; // default "DENY" contentSecurityPolicy?: string | false; // default "default-src 'none'; frame-ancestors 'none'" crossOriginResourcePolicy?: string | false; // default "same-origin" hsts?: { maxAge: number; includeSubDomains?: boolean; preload?: boolean } | false; // default off override?: boolean; // replace a header the route set; default false}rateLimit
Section titled “rateLimit”function rateLimit(options: RateLimitOptions): Middleware;
interface RateLimitOptions { store: RateLimitStore; limit: number; // per window per key windowMs: number; name?: string; // default "default": prefixes the key, to share a store between limits key?: (request: Request) => string | undefined | Promise<string | undefined>; // default: the client address clientIp?: ClientIpOptions; match?: (request: Request) => boolean; // default: every request message?: string; // the 429's detail, default "Too many requests" onStoreError?: "allow" | "block"; // default "allow"; "block" answers 503}
interface RateLimitStore { hit(key: string, windowMs: number): Promise<RateLimitHit>; // add one in the current window, atomically}
interface RateLimitHit { count: number; // including this hit resetAt: number; // epoch milliseconds}Counted responses carry RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset; a 429 adds Retry-After. A limit or windowMs that is not a positive whole number throws RangeError when created.
MemoryRateLimitStore
Section titled “MemoryRateLimitStore”class MemoryRateLimitStore implements RateLimitStore { constructor(options?: MemoryRateLimitStoreOptions); // { maxKeys?: number; now?: () => number }, maxKeys default 100 000}In-process: not shared across replicas. Past maxKeys it drops ended windows and then the oldest keys.
getClientIp
Section titled “getClientIp”function getClientIp(request: Request, options?: ClientIpOptions): string | undefined;
interface ClientIpOptions { trustedProxyHops?: number; // default 0: trust no header isTrustedProxy?: (peer: string) => boolean;}The address that connected, or with trustedProxyHops the address that many entries from the end of X-Forwarded-For. undefined when there is no socket. clientIpFrom(peer, forwardedFor, options) is the same logic as a pure function. Throws RangeError for trustedProxyHops that is not a whole number, 0 or more.
createIpMatcher, ipInCidrs, parseCidr
Section titled “createIpMatcher, ipInCidrs, parseCidr”function createIpMatcher(networks: readonly (string | Cidr)[]): (ip: string | undefined) => boolean;function ipInCidrs(ip: string | undefined, networks: readonly (string | Cidr)[]): boolean; // parses on every callfunction parseCidr(text: string): Cidr; // { family: 4 | 6; prefix: number; network: bigint }Whether an address is inside any of a list of networks ("10.0.0.0/8", "2001:db8::/32", or a bare address for one host). Build a matcher once at startup; it fits isTrustedProxy directly.
- A network that is malformed throws
SecurityConfigErrorwhen the matcher is built. Refused: a prefix that is not plain digits or is out of range, any zone id (%eth0), spaces, leading zeros, hex or single-number addresses (0x7f.0.0.1,2130706433), and an address with bits set after the prefix (192.168.1.5/24: the message says which network was probably meant,192.168.1.0/24). - An address that is not a valid IP (or is
undefined) is never inside, whatever the list holds, and never throws. - An IPv4-mapped IPv6 address (
::ffff:10.0.0.1, what a dual-stack socket reports) is matched as the IPv4 address it carries, and::ffff:10.0.0.0/104as the IPv4 network10.0.0.0/8. IPv4 and IPv6 networks never match each other’s addresses:::/0is every real IPv6 address and no IPv4 one.
Checked against a bit-string model with property tests (every prefix length, both families, several spellings of the same IPv6 address), and against hostile strings; not checked against other libraries’ behaviour.
SecurityConfigError
Section titled “SecurityConfigError”Thrown at creation for configuration that cannot work securely.