UNPKG

@best-skn/elysia-helmet

Version:

A comprehensive security middleware for Elysia.js applications that helps to secure your apps by setting various HTTP headers

191 lines 6.94 kB
import { Elysia } from "elysia"; /** * An object containing permission related constants for some security configurations */ export const permission = { /** Source: Self allowed */ SELF: "'self'", /** Source: Unsafe Inline allowed */ UNSAFE_INLINE: "'unsafe-inline'", /** Source: HTTPS allowed */ HTTPS: "https:", /** Source: Data allowed */ DATA: "data:", /** Source: None is allowed */ NONE: "'none'", /** Source: Blob allowed */ BLOB: "blob:", }; const DEFAULT_CSP_CONFIG = { defaultSrc: [permission.SELF], scriptSrc: [permission.SELF, permission.UNSAFE_INLINE], styleSrc: [permission.SELF, permission.UNSAFE_INLINE], imgSrc: [permission.SELF, permission.DATA, permission.BLOB], fontSrc: [permission.SELF], connectSrc: [permission.SELF], frameSrc: [permission.SELF], objectSrc: [permission.NONE], baseUri: [permission.SELF], }; const DEFAULT_SECURITY_CONFIG = { csp: DEFAULT_CSP_CONFIG, frameOptions: "DENY", xssProtection: true, dnsPrefetch: false, referrerPolicy: "strict-origin-when-cross-origin", permissionsPolicy: { camera: [], microphone: [], geolocation: [], "interest-cohort": [], }, hsts: { maxAge: 15552000, includeSubDomains: true, preload: true, }, corp: "same-origin", coop: "same-origin", }; // Pre-computed regex for camelCase to kebab-case conversion const KEBAB_REGEX = /[A-Z]/g; // Memoized kebab-case conversions const kebabCache = {}; /** * Convert camelCase to kebab-case with memoization */ const toKebabCase = (str) => { if (kebabCache[str]) return kebabCache[str]; const result = str.replace(KEBAB_REGEX, (m) => "-" + m.toLowerCase()); kebabCache[str] = result; return result; }; /** * Validates security configuration */ const validateConfig = (config) => { if (config.hsts?.maxAge && config.hsts.maxAge < 0) { throw new Error("HSTS maxAge must be a positive number"); } if (config.reportTo) { for (const report of config.reportTo) { if (report.maxAge < 0) { throw new Error("Report-To maxAge must be a positive number"); } if (!report.endpoints.length) { throw new Error("Report-To must have at least one endpoint"); } } } }; /** * Generates a nonce for CSP using Base64 encoding * Uses timestamp for better performance */ const generateNonce = () => Buffer.from(Date.now() + Math.random().toString(36).slice(-6)).toString("base64"); /** * Converts CSP config object to CSP header string with optimized string concatenation */ const buildCSPString = (csp, nonce) => { const parts = []; for (const [key, values] of Object.entries(csp)) { if (!values?.length || key === "useNonce" || key === "reportOnly") continue; // Handle nonce injection efficiently if (csp.useNonce && nonce && (key === "scriptSrc" || key === "styleSrc")) { parts.push(`${toKebabCase(key)} ${values.join(" ")} 'nonce-${nonce}'`); continue; } parts.push(`${toKebabCase(key)} ${values.join(" ")}`); } return parts.join("; "); }; /** * Builds Permissions-Policy header string with optimized string building */ const buildPermissionsPolicyString = (policies) => { const parts = []; for (const [key, values] of Object.entries(policies)) { parts.push(values.length === 0 ? `${key}=()` : `${key}=(${values.join(" ")})`); } return parts.join(", "); }; /** * Builds Report-To header string with optimized JSON stringification */ const buildReportToString = (reports) => { // Pre-structure the object to avoid multiple transformations const reportObj = reports.map(({ group, maxAge, endpoints, includeSubdomains }) => ({ group, max_age: maxAge, endpoints, include_subdomains: includeSubdomains, })); return JSON.stringify(reportObj); }; /** * Creates an Elysia middleware that adds security headers to all responses * Optimized for performance with minimal object spread operations */ export const elysiaHelmet = (config = {}) => { // Validate configuration only once during initialization validateConfig(config); const finalConfig = { ...DEFAULT_SECURITY_CONFIG, ...config, csp: config.csp ? { ...DEFAULT_CSP_CONFIG, ...config.csp } : DEFAULT_CSP_CONFIG, permissionsPolicy: config.permissionsPolicy ? { ...DEFAULT_SECURITY_CONFIG.permissionsPolicy, ...config.permissionsPolicy } : DEFAULT_SECURITY_CONFIG.permissionsPolicy, hsts: config.hsts ? { ...DEFAULT_SECURITY_CONFIG.hsts, ...config.hsts } : DEFAULT_SECURITY_CONFIG.hsts, }; const staticHeaders = {}; if (finalConfig.frameOptions) { staticHeaders["X-Frame-Options"] = finalConfig.frameOptions; } if (finalConfig.xssProtection) { staticHeaders["X-XSS-Protection"] = "1; mode=block"; } staticHeaders["X-Content-Type-Options"] = "nosniff"; if (finalConfig.referrerPolicy) { staticHeaders["Referrer-Policy"] = finalConfig.referrerPolicy; } if (finalConfig.dnsPrefetch !== undefined) { staticHeaders["X-DNS-Prefetch-Control"] = finalConfig.dnsPrefetch ? "on" : "off"; } if (finalConfig.corp) { staticHeaders["Cross-Origin-Resource-Policy"] = finalConfig.corp; } if (finalConfig.coop) { staticHeaders["Cross-Origin-Opener-Policy"] = finalConfig.coop; } if (finalConfig.reportTo) { staticHeaders["Report-To"] = buildReportToString(finalConfig.reportTo); } if (finalConfig.customHeaders) { Object.assign(staticHeaders, finalConfig.customHeaders); } const isProduction = process.env.NODE_ENV === "production"; const hstsHeader = isProduction && finalConfig.hsts ? `max-age=${finalConfig.hsts.maxAge}${finalConfig.hsts.includeSubDomains ? "; includeSubDomains" : ""}${finalConfig.hsts.preload ? "; preload" : ""}` : null; return new Elysia().derive({ as: "global" }, ({ set }) => { set.headers = { ...staticHeaders }; if (finalConfig.csp) { const nonce = finalConfig.csp.useNonce ? generateNonce() : undefined; const headerName = finalConfig.csp.reportOnly ? "Content-Security-Policy-Report-Only" : "Content-Security-Policy"; set.headers[headerName] = buildCSPString(finalConfig.csp, nonce); if (nonce) { set.headers["X-Nonce"] = nonce; } } if (finalConfig.permissionsPolicy) { set.headers["Permissions-Policy"] = buildPermissionsPolicyString(finalConfig.permissionsPolicy); } if (hstsHeader) { set.headers["Strict-Transport-Security"] = hstsHeader; } }); }; //# sourceMappingURL=index.js.map