UNPKG

@ordojs/security

Version:

Security package for OrdoJS with XSS, CSRF, and injection protection

150 lines (127 loc) 3.82 kB
/** * Escape special HTML characters to prevent XSS attacks in template interpolations */ /** * Map of characters that need to be escaped in HTML */ const HTML_ESCAPE_MAP: Record<string, string> = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;', '/': '&#x2F;', '`': '&#x60;', '=': '&#x3D;', }; /** * Regular expression to match characters that need to be escaped */ const HTML_ESCAPE_REGEX = /[&<>"'`=\/]/g; /** * Escape HTML special characters to prevent XSS attacks * @param value String value to escape * @returns Escaped HTML string */ export function escapeHtml(value: string): string { return String(value).replace(HTML_ESCAPE_REGEX, (char) => HTML_ESCAPE_MAP[char] || char); } /** * Options for the template escaper */ export interface TemplateEscaperOptions { /** * Whether to escape all interpolated values by default * @default true */ escapeByDefault: boolean; /** * Whether to allow raw HTML through the raw tag function * @default false */ allowRawHtml: boolean; } /** * Template escaper for automatic HTML escaping in template interpolations */ export class TemplateEscaper { private options: TemplateEscaperOptions; /** * Create a new TemplateEscaper instance * @param options Template escaper options */ constructor(options: Partial<TemplateEscaperOptions> = {}) { this.options = { escapeByDefault: true, allowRawHtml: false, ...options, }; } /** * Escape a value for safe HTML interpolation * @param value Value to escape * @returns Escaped value */ escape(value: unknown): string { if (value === null || value === undefined) { return ''; } if (typeof value === 'object') { return escapeHtml(JSON.stringify(value)); } return escapeHtml(String(value)); } /** * Create a tagged template function that automatically escapes interpolated values * @returns Tagged template function */ createEscapedTemplate(): (strings: TemplateStringsArray, ...values: unknown[]) => string { return (strings: TemplateStringsArray, ...values: unknown[]): string => { let result = ''; for (let i = 0; i < strings.length; i++) { result += strings[i]; if (i < values.length) { result += this.escape(values[i]); } } return result; }; } /** * Create a tagged template function that allows raw HTML (use with caution) * @returns Tagged template function for raw HTML * @throws Error if raw HTML is not allowed in options */ createRawTemplate(): (strings: TemplateStringsArray, ...values: unknown[]) => string { if (!this.options.allowRawHtml) { throw new Error('Raw HTML templates are not allowed. Set allowRawHtml: true in options to enable.'); } return (strings: TemplateStringsArray, ...values: unknown[]): string => { let result = ''; for (let i = 0; i < strings.length; i++) { result += strings[i]; if (i < values.length) { result += String(values[i]); } } return result; }; } } /** * Default template escaper instance */ export const defaultTemplateEscaper = new TemplateEscaper(); /** * Tagged template function that automatically escapes interpolated values * @example html\`<div>\${userInput}</div>\` // userInput is automatically escaped */ export const html = defaultTemplateEscaper.createEscapedTemplate(); /** * Create a raw HTML tagged template (use with caution) * Must be explicitly enabled by setting allowRawHtml: true */ export function createRawHtmlTemplate(): (strings: TemplateStringsArray, ...values: unknown[]) => string { const escaper = new TemplateEscaper({ allowRawHtml: true }); return escaper.createRawTemplate(); }