UNPKG

remix-utils

Version:

This package contains simple utility functions to use with [React Router](https://reactrouter.com/).

155 lines 7.54 kB
/** * Secure Headers Middleware for React-router. * * @param {Partial<SecureHeadersOptions>} [customOptions] - The options for the secure headers middleware. * @param {ContentSecurityPolicyOptions} [customOptions.contentSecurityPolicy] - Settings for the Content-Security-Policy header. * @param {ContentSecurityPolicyOptions} [customOptions.contentSecurityPolicyReportOnly] - Settings for the Content-Security-Policy-Report-Only header. * @param {overridableHeader} [customOptions.crossOriginEmbedderPolicy=false] - Settings for the Cross-Origin-Embedder-Policy header. * @param {overridableHeader} [customOptions.crossOriginResourcePolicy=true] - Settings for the Cross-Origin-Resource-Policy header. * @param {overridableHeader} [customOptions.crossOriginOpenerPolicy=true] - Settings for the Cross-Origin-Opener-Policy header. * @param {overridableHeader} [customOptions.originAgentCluster=true] - Settings for the Origin-Agent-Cluster header. * @param {overridableHeader} [customOptions.referrerPolicy=true] - Settings for the Referrer-Policy header. * @param {ReportingEndpointOptions[]} [customOptions.reportingEndpoints] - Settings for the Reporting-Endpoints header. * @param {ReportToOptions[]} [customOptions.reportTo] - Settings for the Report-To header. * @param {overridableHeader} [customOptions.strictTransportSecurity=true] - Settings for the Strict-Transport-Security header. * @param {overridableHeader} [customOptions.xContentTypeOptions=true] - Settings for the X-Content-Type-Options header. * @param {overridableHeader} [customOptions.xDnsPrefetchControl=true] - Settings for the X-DNS-Prefetch-Control header. * @param {overridableHeader} [customOptions.xDownloadOptions=true] - Settings for the X-Download-Options header. * @param {overridableHeader} [customOptions.xFrameOptions=true] - Settings for the X-Frame-Options header. * @param {overridableHeader} [customOptions.xPermittedCrossDomainPolicies=true] - Settings for the X-Permitted-Cross-Domain-Policies header. * @param {overridableHeader} [customOptions.xXssProtection=true] - Settings for the X-XSS-Protection header. * @param {boolean} [customOptions.removePoweredBy=true] - Settings for remove X-Powered-By header. * @param {PermissionsPolicyOptions} [customOptions.permissionsPolicy] - Settings for the Permissions-Policy header. * @returns {MiddlewareHandler} The middleware handler function. */ export function unstable_createSecureHeadersMiddleware(customOptions) { let options = { ...DEFAULT_OPTIONS, ...customOptions }; let headersToSet = getFilteredHeaders(options); if (options.contentSecurityPolicy) { let value = getCSPDirectives(options.contentSecurityPolicy); headersToSet.push(["Content-Security-Policy", value]); } if (options.contentSecurityPolicyReportOnly) { let value = getCSPDirectives(options.contentSecurityPolicyReportOnly); 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 secureHeaders(_, next) { // should evaluate callbacks before next() // some callback calls ctx.set() for embedding nonce to the page let response = await next(); setHeaders(response, headersToSet); if (options?.removePoweredBy) { response.headers.delete("X-Powered-By"); } return response; }, ]; } const 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"], }; const 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: {}, }; function getFilteredHeaders(options) { return Object.entries(HEADERS_MAP) .filter(([key]) => options[key]) .map(([key, defaultValue]) => { let overrideValue = options[key]; return typeof overrideValue === "string" ? [defaultValue[0], overrideValue] : defaultValue; }); } function getCSPDirectives(contentSecurityPolicy) { let resultValues = []; for (let [directive, value] of Object.entries(contentSecurityPolicy)) { let valueArray = Array.isArray(value) ? value : [value]; resultValues.push(directive.replace(/[A-Z]+(?![a-z])|[A-Z]/g, (match, offset) => offset ? `-${match.toLowerCase()}` : match.toLowerCase()), ...valueArray.flatMap((value) => [" ", value]), "; "); } resultValues.pop(); return resultValues.join(""); } function getPermissionsPolicyDirectives(policy) { return Object.entries(policy) .map(([directive, value]) => { let 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]}`; } let 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(response, headersToSet) { for (let [header, value] of headersToSet) { response.headers.set(header, value); } } //# sourceMappingURL=secure-headers.js.map