UNPKG

hono

Version:

Web framework built on Web Standards

166 lines (165 loc) 5.99 kB
// src/middleware/secure-headers/secure-headers.ts import { encodeBase64 } from "../../utils/encode.js"; var HEADERS_MAP = { crossOriginEmbedderPolicy: ["Cross-Origin-Embedder-Policy", "require-corp"], crossOriginResourcePolicy: ["Cross-Origin-Resource-Policy", "same-origin"], crossOriginOpenerPolicy: ["Cross-Origin-Opener-Policy", "same-origin"], originAgentCluster: ["Origin-Agent-Cluster", "?1"], referrerPolicy: ["Referrer-Policy", "no-referrer"], strictTransportSecurity: ["Strict-Transport-Security", "max-age=15552000; includeSubDomains"], xContentTypeOptions: ["X-Content-Type-Options", "nosniff"], xDnsPrefetchControl: ["X-DNS-Prefetch-Control", "off"], xDownloadOptions: ["X-Download-Options", "noopen"], xFrameOptions: ["X-Frame-Options", "SAMEORIGIN"], xPermittedCrossDomainPolicies: ["X-Permitted-Cross-Domain-Policies", "none"], xXssProtection: ["X-XSS-Protection", "0"] }; var DEFAULT_OPTIONS = { crossOriginEmbedderPolicy: false, crossOriginResourcePolicy: true, crossOriginOpenerPolicy: true, originAgentCluster: true, referrerPolicy: true, strictTransportSecurity: true, xContentTypeOptions: true, xDnsPrefetchControl: true, xDownloadOptions: true, xFrameOptions: true, xPermittedCrossDomainPolicies: true, xXssProtection: true, removePoweredBy: true, permissionsPolicy: {} }; var generateNonce = () => { const buffer = new Uint8Array(16); crypto.getRandomValues(buffer); return encodeBase64(buffer); }; var NONCE = (ctx) => { const nonce = ctx.get("secureHeadersNonce") || (() => { const newNonce = generateNonce(); ctx.set("secureHeadersNonce", newNonce); return newNonce; })(); return `'nonce-${nonce}'`; }; var secureHeaders = (customOptions) => { const options = { ...DEFAULT_OPTIONS, ...customOptions }; const headersToSet = getFilteredHeaders(options); const callbacks = []; if (options.contentSecurityPolicy) { const [callback, value] = getCSPDirectives(options.contentSecurityPolicy); if (callback) { callbacks.push(callback); } headersToSet.push(["Content-Security-Policy", value]); } if (options.contentSecurityPolicyReportOnly) { const [callback, value] = getCSPDirectives(options.contentSecurityPolicyReportOnly); if (callback) { callbacks.push(callback); } headersToSet.push(["Content-Security-Policy-Report-Only", value]); } if (options.permissionsPolicy && Object.keys(options.permissionsPolicy).length > 0) { headersToSet.push([ "Permissions-Policy", getPermissionsPolicyDirectives(options.permissionsPolicy) ]); } if (options.reportingEndpoints) { headersToSet.push(["Reporting-Endpoints", getReportingEndpoints(options.reportingEndpoints)]); } if (options.reportTo) { headersToSet.push(["Report-To", getReportToOptions(options.reportTo)]); } return async function secureHeaders2(ctx, next) { const headersToSetForReq = callbacks.length === 0 ? headersToSet : callbacks.reduce((acc, cb) => cb(ctx, acc), headersToSet); await next(); setHeaders(ctx, headersToSetForReq); if (options?.removePoweredBy) { ctx.res.headers.delete("X-Powered-By"); } }; }; function getFilteredHeaders(options) { return Object.entries(HEADERS_MAP).filter(([key]) => options[key]).map(([key, defaultValue]) => { const overrideValue = options[key]; return typeof overrideValue === "string" ? [defaultValue[0], overrideValue] : defaultValue; }); } function getCSPDirectives(contentSecurityPolicy) { const callbacks = []; const resultValues = []; for (const [directive, value] of Object.entries(contentSecurityPolicy)) { const valueArray = Array.isArray(value) ? value : [value]; valueArray.forEach((value2, i) => { if (typeof value2 === "function") { const index = i * 2 + 2 + resultValues.length; callbacks.push((ctx, values) => { values[index] = value2(ctx, directive); }); } }); resultValues.push( directive.replace( /[A-Z]+(?![a-z])|[A-Z]/g, (match, offset) => offset ? "-" + match.toLowerCase() : match.toLowerCase() ), ...valueArray.flatMap((value2) => [" ", value2]), "; " ); } resultValues.pop(); return callbacks.length === 0 ? [void 0, resultValues.join("")] : [ (ctx, headersToSet) => headersToSet.map((values) => { if (values[0] === "Content-Security-Policy" || values[0] === "Content-Security-Policy-Report-Only") { const clone = values[1].slice(); callbacks.forEach((cb) => { cb(ctx, clone); }); return [values[0], clone.join("")]; } else { return values; } }), resultValues ]; } function getPermissionsPolicyDirectives(policy) { return Object.entries(policy).map(([directive, value]) => { const kebabDirective = camelToKebab(directive); if (typeof value === "boolean") { return `${kebabDirective}=${value ? "*" : "none"}`; } if (Array.isArray(value)) { if (value.length === 0) { return `${kebabDirective}=()`; } if (value.length === 1 && (value[0] === "*" || value[0] === "none")) { return `${kebabDirective}=${value[0]}`; } const allowlist = value.map((item) => ["self", "src"].includes(item) ? item : `"${item}"`); return `${kebabDirective}=(${allowlist.join(" ")})`; } return ""; }).filter(Boolean).join(", "); } function camelToKebab(str) { return str.replace(/([a-z\d])([A-Z])/g, "$1-$2").toLowerCase(); } function getReportingEndpoints(reportingEndpoints = []) { return reportingEndpoints.map((endpoint) => `${endpoint.name}="${endpoint.url}"`).join(", "); } function getReportToOptions(reportTo = []) { return reportTo.map((option) => JSON.stringify(option)).join(", "); } function setHeaders(ctx, headersToSet) { headersToSet.forEach(([header, value]) => { ctx.res.headers.set(header, value); }); } export { NONCE, secureHeaders };