@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
JavaScript
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