UNPKG

nuxt-security

Version:

🛡️ Security Module for Nuxt based on HTTP Headers and Middleware

160 lines (158 loc) 6.2 kB
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 };