UNPKG

@ordojs/security

Version:

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

251 lines (227 loc) 6.09 kB
/** * Content Security Policy (CSP) manager for XSS protection */ /** * CSP directive types */ export type CspDirective = | 'default-src' | 'script-src' | 'style-src' | 'img-src' | 'font-src' | 'connect-src' | 'media-src' | 'object-src' | 'frame-src' | 'child-src' | 'worker-src' | 'manifest-src' | 'base-uri' | 'form-action' | 'frame-ancestors' | 'plugin-types' | 'sandbox' | 'upgrade-insecure-requests' | 'block-all-mixed-content'; /** * CSP source values */ export type CspSource = | "'self'" | "'unsafe-inline'" | "'unsafe-eval'" | "'strict-dynamic'" | "'none'" | string; // URLs, nonces, hashes /** * CSP policy configuration */ export interface CspPolicy { [directive: string]: CspSource[]; } /** * CSP manager options */ export interface CspManagerOptions { /** * Whether to report CSP violations * @default false */ reportViolations?: boolean; /** * URL to send CSP violation reports to */ reportUri?: string; /** * Whether to use CSP in report-only mode * @default false */ reportOnly?: boolean; } /** * Default secure CSP policy */ const DEFAULT_CSP_POLICY: CspPolicy = { 'default-src': ["'self'"], 'script-src': ["'self'"], 'style-src': ["'self'", "'unsafe-inline'"], // Allow inline styles for component styling 'img-src': ["'self'", 'data:', 'https:'], 'font-src': ["'self'", 'https:'], 'connect-src': ["'self'"], 'media-src': ["'self'"], 'object-src': ["'none'"], 'frame-src': ["'none'"], 'base-uri': ["'self'"], 'form-action': ["'self'"], 'frame-ancestors': ["'none'"], 'upgrade-insecure-requests': [], }; /** * Content Security Policy manager for XSS protection */ export class CspManager { private policy: CspPolicy; private options: CspManagerOptions; private nonces: Set<string> = new Set(); /** * Create a new CSP manager * @param policy Initial CSP policy * @param options CSP manager options */ constructor(policy: Partial<CspPolicy> = {}, options: CspManagerOptions = {}) { this.policy = { ...DEFAULT_CSP_POLICY }; // Merge user policy, ensuring all values are arrays Object.entries(policy).forEach(([key, value]) => { if (value) { this.policy[key] = value; } }); this.options = { reportViolations: false, reportOnly: false, ...options, }; } /** * Generate a cryptographically secure nonce for inline scripts/styles * @returns Base64 encoded nonce */ generateNonce(): string { const array = new Uint8Array(16); crypto.getRandomValues(array); const nonce = btoa(String.fromCharCode(...array)); this.nonces.add(nonce); return nonce; } /** * Add a nonce to the script-src directive * @param nonce The nonce to add */ addScriptNonce(nonce: string): void { this.addSourceToDirective('script-src', `'nonce-${nonce}'`); } /** * Add a nonce to the style-src directive * @param nonce The nonce to add */ addStyleNonce(nonce: string): void { this.addSourceToDirective('style-src', `'nonce-${nonce}'`); } /** * Add a hash to a directive for inline content * @param directive The CSP directive * @param hash The SHA hash (e.g., 'sha256-abc123...') */ addHash(directive: CspDirective, hash: string): void { this.addSourceToDirective(directive, `'${hash}'`); } /** * Add a source to a CSP directive * @param directive The CSP directive * @param source The source to add */ addSourceToDirective(directive: CspDirective, source: CspSource): void { if (!this.policy[directive]) { this.policy[directive] = []; } if (!this.policy[directive].includes(source)) { this.policy[directive].push(source); } } /** * Remove a source from a CSP directive * @param directive The CSP directive * @param source The source to remove */ removeSourceFromDirective(directive: CspDirective, source: CspSource): void { if (this.policy[directive]) { this.policy[directive] = this.policy[directive].filter(s => s !== source); } } /** * Generate the CSP header value * @returns CSP header value string */ generateHeader(): string { const directives = Object.entries(this.policy) .filter(([_, sources]) => sources.length > 0) .map(([directive, sources]) => { if (sources.length === 0) { return directive; } return `${directive} ${sources.join(' ')}`; }); if (this.options.reportUri) { directives.push(`report-uri ${this.options.reportUri}`); } return directives.join('; '); } /** * Get the appropriate CSP header name * @returns CSP header name */ getHeaderName(): string { return this.options.reportOnly ? 'Content-Security-Policy-Report-Only' : 'Content-Security-Policy'; } /** * Create a strict CSP policy for maximum security * @returns New CspManager with strict policy */ static createStrict(): CspManager { return new CspManager({ 'default-src': ["'none'"], 'script-src': ["'self'"], 'style-src': ["'self'"], 'img-src': ["'self'"], 'font-src': ["'self'"], 'connect-src': ["'self'"], 'media-src': ["'none'"], 'object-src': ["'none'"], 'frame-src': ["'none'"], 'base-uri': ["'none'"], 'form-action': ["'self'"], 'frame-ancestors': ["'none'"], }); } /** * Create a development-friendly CSP policy * @returns New CspManager with development policy */ static createDevelopment(): CspManager { return new CspManager({ 'default-src': ["'self'"], 'script-src': ["'self'", "'unsafe-eval'"], // Allow eval for dev tools 'style-src': ["'self'", "'unsafe-inline'"], 'img-src': ["'self'", 'data:', 'https:', 'http:'], 'font-src': ["'self'", 'https:', 'data:'], 'connect-src': ["'self'", 'ws:', 'wss:'], // Allow WebSocket for HMR }); } } /** * Default CSP manager instance */ export const defaultCspManager = new CspManager();