126 lines
4.4 KiB
JavaScript
126 lines
4.4 KiB
JavaScript
// src/middleware/secure-headers/index.ts
|
|
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
|
|
};
|
|
var generateNonce = () => {
|
|
const buffer = new Uint8Array(16);
|
|
crypto.getRandomValues(buffer);
|
|
return Buffer.from(buffer).toString("base64");
|
|
};
|
|
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.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);
|
|
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") {
|
|
const clone = values[1].slice();
|
|
callbacks.forEach((cb) => {
|
|
cb(ctx, clone);
|
|
});
|
|
return [values[0], clone.join("")];
|
|
} else {
|
|
return values;
|
|
}
|
|
}),
|
|
resultValues
|
|
];
|
|
}
|
|
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
|
|
};
|