remix-utils
Version:
This package contains simple utility functions to use with [React Router](https://reactrouter.com/).
155 lines • 7.54 kB
JavaScript
/**
* 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