@ordojs/security
Version:
Security package for OrdoJS with XSS, CSRF, and injection protection
251 lines (227 loc) • 6.09 kB
text/typescript
/**
* 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();