nuxt-security
Version:
🛡️ Security Module for Nuxt based on HTTP Headers and Middleware
160 lines (158 loc) • 6.2 kB
JavaScript
const KEYS_TO_NAMES = {
contentSecurityPolicy: "Content-Security-Policy",
crossOriginEmbedderPolicy: "Cross-Origin-Embedder-Policy",
crossOriginOpenerPolicy: "Cross-Origin-Opener-Policy",
crossOriginResourcePolicy: "Cross-Origin-Resource-Policy",
originAgentCluster: "Origin-Agent-Cluster",
referrerPolicy: "Referrer-Policy",
strictTransportSecurity: "Strict-Transport-Security",
xContentTypeOptions: "X-Content-Type-Options",
xDNSPrefetchControl: "X-DNS-Prefetch-Control",
xDownloadOptions: "X-Download-Options",
xFrameOptions: "X-Frame-Options",
xPermittedCrossDomainPolicies: "X-Permitted-Cross-Domain-Policies",
xXSSProtection: "X-XSS-Protection",
permissionsPolicy: "Permissions-Policy"
};
const NAMES_TO_KEYS = Object.fromEntries(Object.entries(KEYS_TO_NAMES).map(([key, name]) => [name, key]));
function getNameFromKey(key) {
return KEYS_TO_NAMES[key];
}
function getKeyFromName(headerName) {
const [, key] = Object.entries(NAMES_TO_KEYS).find(([name]) => name.toLowerCase() === headerName.toLowerCase()) || [];
return key;
}
function headerStringFromObject(optionKey, optionValue) {
if (optionValue === false) {
return "";
}
if (optionKey === "contentSecurityPolicy") {
const policies = optionValue;
return Object.entries(policies).filter(([, value]) => value !== false).map(([directive, sources]) => {
if (directive === "upgrade-insecure-requests") {
return "upgrade-insecure-requests;";
} else {
const stringifiedSources = typeof sources === "string" ? sources : sources.map((source) => source.trim()).join(" ");
return `${directive} ${stringifiedSources};`;
}
}).join(" ");
} else if (optionKey === "strictTransportSecurity") {
const policies = optionValue;
return [
`max-age=${policies.maxAge}`,
policies.includeSubdomains && "includeSubDomains",
policies.preload && "preload"
].filter(Boolean).join("; ");
} else if (optionKey === "permissionsPolicy") {
const policies = optionValue;
return Object.entries(policies).filter(([, value]) => value !== false).map(([directive, sources]) => {
if (typeof sources === "string") {
return `${directive}=${sources}`;
} else {
return `${directive}=(${sources.join(" ")})`;
}
}).join(", ");
} else {
return optionValue;
}
}
function headerObjectFromString(optionKey, headerValue) {
if (!headerValue) {
return false;
}
if (optionKey === "contentSecurityPolicy") {
const directives = headerValue.split(";").map((directive) => directive.trim()).filter((directive) => directive);
const objectForm = {};
for (const directive of directives) {
const [type, ...sources] = directive.split(" ").map((token) => token.trim());
if (type === "upgrade-insecure-requests") {
objectForm[type] = true;
} else {
objectForm[type] = sources.join(" ");
}
}
return objectForm;
} else if (optionKey === "strictTransportSecurity") {
const directives = headerValue.split(";").map((directive) => directive.trim()).filter((directive) => directive);
const objectForm = {};
for (const directive of directives) {
const [type, value] = directive.split("=").map((token) => token.trim());
if (type === "max-age") {
objectForm.maxAge = Number(value);
} else if (type === "includeSubdomains" || type === "preload") {
objectForm[type] = true;
}
}
return objectForm;
} else if (optionKey === "permissionsPolicy") {
const directives = headerValue.split(",").map((directive) => directive.trim()).filter((directive) => directive);
const objectForm = {};
for (const directive of directives) {
const [type, value] = directive.split("=").map((token) => token.trim());
objectForm[type] = value;
}
return objectForm;
} else {
return headerValue;
}
}
function appliesToAllResources(optionKey) {
switch (optionKey) {
case "referrerPolicy":
case "strictTransportSecurity":
case "xContentTypeOptions":
case "xDownloadOptions":
case "xFrameOptions":
case "xPermittedCrossDomainPolicies":
case "xXSSProtection":
return true;
default:
return false;
}
}
function getHeadersApplicableToAllResources(headers) {
const applicableHeaders = Object.fromEntries(
Object.entries(headers).filter(([key]) => appliesToAllResources(key)).map(([key, value]) => [getNameFromKey(key), headerStringFromObject(key, value)]).filter(([, value]) => Boolean(value))
);
return Object.keys(applicableHeaders).length === 0 ? void 0 : applicableHeaders;
}
function standardToSecurity(standardHeaders) {
if (!standardHeaders) {
return void 0;
}
const standardHeadersAsObject = {};
Object.entries(standardHeaders).forEach(([headerName, headerValue]) => {
const optionKey = getKeyFromName(headerName);
if (optionKey) {
if (typeof headerValue === "string") {
const objectValue = headerObjectFromString(optionKey, headerValue);
standardHeadersAsObject[optionKey] = objectValue;
} else {
standardHeadersAsObject[optionKey] = headerValue;
}
}
});
if (Object.keys(standardHeadersAsObject).length === 0) {
return void 0;
}
return standardHeadersAsObject;
}
function backwardsCompatibleSecurity(securityHeaders) {
if (!securityHeaders) {
return void 0;
}
const securityHeadersAsObject = {};
Object.entries(securityHeaders).forEach(([key, value]) => {
const optionKey = key;
if ((optionKey === "contentSecurityPolicy" || optionKey === "permissionsPolicy" || optionKey === "strictTransportSecurity") && typeof value === "string") {
const objectValue = headerObjectFromString(optionKey, value);
securityHeadersAsObject[optionKey] = objectValue;
} else if (value === "") {
securityHeadersAsObject[optionKey] = false;
} else {
securityHeadersAsObject[optionKey] = value;
}
});
return securityHeadersAsObject;
}
export { backwardsCompatibleSecurity, getHeadersApplicableToAllResources, getKeyFromName, getNameFromKey, headerObjectFromString, headerStringFromObject, standardToSecurity };