websecure-ez
Version:
🛡️ The ultimate web security configuration tool and library for Next.js applications. Features a powerful security middleware library, visual configuration interface, and CLI with industry-specific templates.
459 lines (431 loc) • 14.6 kB
text/typescript
import { NextRequest, NextResponse } from 'next/server';
export interface WebSecureConfig {
contentSecurityPolicy?: {
enabled: boolean;
directives?: {
defaultSrc?: string[];
scriptSrc?: string[];
styleSrc?: string[];
imgSrc?: string[];
connectSrc?: string[];
fontSrc?: string[];
objectSrc?: string[];
mediaSrc?: string[];
frameSrc?: string[];
childSrc?: string[];
workerSrc?: string[];
manifestSrc?: string[];
formAction?: string[];
frameAncestors?: string[];
baseUri?: string[];
upgradeInsecureRequests?: boolean;
blockAllMixedContent?: boolean;
};
reportOnly?: boolean;
reportUri?: string;
};
xFrameOptions?: {
enabled: boolean;
option?: 'DENY' | 'SAMEORIGIN' | string; // Allow custom values for allow-from
};
secureCookies?: {
enabled: boolean;
httpOnly?: boolean;
secure?: boolean;
sameSite?: 'Strict' | 'Lax' | 'None';
};
referrerPolicy?: {
enabled: boolean;
policy?: 'no-referrer' | 'no-referrer-when-downgrade' | 'origin' | 'origin-when-cross-origin' | 'same-origin' | 'strict-origin' | 'strict-origin-when-cross-origin' | 'unsafe-url';
};
permissionsPolicy?: {
enabled: boolean;
features?: Record<string, string | string[]>;
};
xContentTypeOptions?: {
enabled: boolean;
};
xssProtection?: {
enabled: boolean;
mode?: 'block' | 'report';
reportUri?: string;
};
hsts?: {
enabled: boolean;
maxAge?: number;
includeSubDomains?: boolean;
preload?: boolean;
};
expectCt?: {
enabled: boolean;
maxAge?: number;
enforce?: boolean;
reportUri?: string;
};
crossOriginEmbedderPolicy?: {
enabled: boolean;
policy?: 'unsafe-none' | 'require-corp';
};
crossOriginOpenerPolicy?: {
enabled: boolean;
policy?: 'unsafe-none' | 'same-origin-allow-popups' | 'same-origin';
};
crossOriginResourcePolicy?: {
enabled: boolean;
policy?: 'same-site' | 'same-origin' | 'cross-origin';
};
}
const defaultConfig: WebSecureConfig = {
contentSecurityPolicy: {
enabled: true,
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", 'data:', 'https:'],
connectSrc: ["'self'"],
fontSrc: ["'self'", 'https:', 'data:'],
objectSrc: ["'none'"],
mediaSrc: ["'self'"],
frameSrc: ["'none'"],
childSrc: ["'self'"],
workerSrc: ["'self'"],
manifestSrc: ["'self'"],
formAction: ["'self'"],
frameAncestors: ["'none'"],
baseUri: ["'self'"],
upgradeInsecureRequests: true,
},
reportOnly: false,
},
xFrameOptions: {
enabled: true,
option: 'DENY',
},
secureCookies: {
enabled: true,
httpOnly: true,
secure: true,
sameSite: 'Strict',
},
referrerPolicy: {
enabled: true,
policy: 'strict-origin-when-cross-origin',
},
permissionsPolicy: {
enabled: true,
features: {
camera: "'none'",
microphone: "'none'",
geolocation: "'none'",
'payment': "'none'",
'usb': "'none'",
'vr': "'none'",
'magnetometer': "'none'",
'gyroscope': "'none'",
'speaker': "'none'",
'vibrate': "'none'",
},
},
xContentTypeOptions: {
enabled: true,
},
xssProtection: {
enabled: true,
mode: 'block',
},
hsts: {
enabled: true,
maxAge: 31536000, // 1 year
includeSubDomains: true,
preload: true,
},
expectCt: {
enabled: false,
maxAge: 86400, // 24 hours
enforce: false,
},
crossOriginEmbedderPolicy: {
enabled: false,
policy: 'unsafe-none',
},
crossOriginOpenerPolicy: {
enabled: false,
policy: 'same-origin-allow-popups',
},
crossOriginResourcePolicy: {
enabled: false,
policy: 'same-site',
},
};
export function createSecureMiddleware(config: Partial<WebSecureConfig> = {}) {
// Merge with default config
const mergedConfig: WebSecureConfig = {
contentSecurityPolicy: {
enabled: config.contentSecurityPolicy?.enabled ?? defaultConfig.contentSecurityPolicy?.enabled ?? false,
...defaultConfig.contentSecurityPolicy,
...config.contentSecurityPolicy,
directives: {
...defaultConfig.contentSecurityPolicy?.directives,
...config.contentSecurityPolicy?.directives,
},
},
xFrameOptions: {
enabled: config.xFrameOptions?.enabled ?? defaultConfig.xFrameOptions?.enabled ?? false,
...defaultConfig.xFrameOptions,
...config.xFrameOptions,
},
secureCookies: {
enabled: config.secureCookies?.enabled ?? defaultConfig.secureCookies?.enabled ?? false,
...defaultConfig.secureCookies,
...config.secureCookies,
},
referrerPolicy: {
enabled: config.referrerPolicy?.enabled ?? defaultConfig.referrerPolicy?.enabled ?? false,
...defaultConfig.referrerPolicy,
...config.referrerPolicy,
},
permissionsPolicy: {
enabled: config.permissionsPolicy?.enabled ?? defaultConfig.permissionsPolicy?.enabled ?? false,
...defaultConfig.permissionsPolicy,
...config.permissionsPolicy,
features: {
...defaultConfig.permissionsPolicy?.features,
...config.permissionsPolicy?.features,
},
},
xContentTypeOptions: {
enabled: config.xContentTypeOptions?.enabled ?? defaultConfig.xContentTypeOptions?.enabled ?? false,
...defaultConfig.xContentTypeOptions,
...config.xContentTypeOptions,
},
xssProtection: {
enabled: config.xssProtection?.enabled ?? defaultConfig.xssProtection?.enabled ?? false,
...defaultConfig.xssProtection,
...config.xssProtection,
},
hsts: {
enabled: config.hsts?.enabled ?? defaultConfig.hsts?.enabled ?? false,
...defaultConfig.hsts,
...config.hsts,
},
expectCt: {
enabled: config.expectCt?.enabled ?? defaultConfig.expectCt?.enabled ?? false,
...defaultConfig.expectCt,
...config.expectCt,
},
crossOriginEmbedderPolicy: {
enabled: config.crossOriginEmbedderPolicy?.enabled ?? defaultConfig.crossOriginEmbedderPolicy?.enabled ?? false,
...defaultConfig.crossOriginEmbedderPolicy,
...config.crossOriginEmbedderPolicy,
},
crossOriginOpenerPolicy: {
enabled: config.crossOriginOpenerPolicy?.enabled ?? defaultConfig.crossOriginOpenerPolicy?.enabled ?? false,
...defaultConfig.crossOriginOpenerPolicy,
...config.crossOriginOpenerPolicy,
},
crossOriginResourcePolicy: {
enabled: config.crossOriginResourcePolicy?.enabled ?? defaultConfig.crossOriginResourcePolicy?.enabled ?? false,
...defaultConfig.crossOriginResourcePolicy,
...config.crossOriginResourcePolicy,
},
};
return function middleware(request: NextRequest) {
const response = NextResponse.next();
const headers = response.headers;
// Apply Content-Security-Policy
if (mergedConfig.contentSecurityPolicy?.enabled) {
const directives = mergedConfig.contentSecurityPolicy.directives;
if (directives) {
const cspParts: string[] = [];
Object.entries(directives).forEach(([key, value]) => {
if (key === 'upgradeInsecureRequests' && value === true) {
cspParts.push('upgrade-insecure-requests');
} else if (key === 'blockAllMixedContent' && value === true) {
cspParts.push('block-all-mixed-content');
} else if (Array.isArray(value) && value.length > 0) {
// Convert camelCase to kebab-case
const kebabKey = key.replace(/([A-Z])/g, '-$1').toLowerCase();
cspParts.push(`${kebabKey} ${value.join(' ')}`);
}
});
if (cspParts.length > 0) {
const cspValue = cspParts.join('; ');
const headerName = mergedConfig.contentSecurityPolicy.reportOnly
? 'Content-Security-Policy-Report-Only'
: 'Content-Security-Policy';
headers.set(headerName, cspValue);
}
}
}
// Apply X-Frame-Options
if (mergedConfig.xFrameOptions?.enabled && mergedConfig.xFrameOptions.option) {
headers.set('X-Frame-Options', mergedConfig.xFrameOptions.option);
}
// Apply Referrer-Policy
if (mergedConfig.referrerPolicy?.enabled && mergedConfig.referrerPolicy.policy) {
headers.set('Referrer-Policy', mergedConfig.referrerPolicy.policy);
}
// Apply Permissions-Policy
if (mergedConfig.permissionsPolicy?.enabled && mergedConfig.permissionsPolicy.features) {
const features = mergedConfig.permissionsPolicy.features;
const permissionsValue = Object.entries(features)
.map(([feature, value]) => {
if (Array.isArray(value)) {
return `${feature}=(${value.join(' ')})`;
}
return `${feature}=${value}`;
})
.join(', ');
if (permissionsValue) {
headers.set('Permissions-Policy', permissionsValue);
}
}
// Apply X-Content-Type-Options
if (mergedConfig.xContentTypeOptions?.enabled) {
headers.set('X-Content-Type-Options', 'nosniff');
}
// Apply X-XSS-Protection
if (mergedConfig.xssProtection?.enabled) {
const mode = mergedConfig.xssProtection.mode || 'block';
let xssValue = '1';
if (mode === 'block') {
xssValue += '; mode=block';
} else if (mode === 'report' && mergedConfig.xssProtection.reportUri) {
xssValue += `; report=${mergedConfig.xssProtection.reportUri}`;
}
headers.set('X-XSS-Protection', xssValue);
}
// Apply HSTS (HTTP Strict Transport Security)
if (mergedConfig.hsts?.enabled && request.nextUrl.protocol === 'https:') {
let hstsValue = `max-age=${mergedConfig.hsts.maxAge || 31536000}`;
if (mergedConfig.hsts.includeSubDomains) {
hstsValue += '; includeSubDomains';
}
if (mergedConfig.hsts.preload) {
hstsValue += '; preload';
}
headers.set('Strict-Transport-Security', hstsValue);
}
// Apply Expect-CT
if (mergedConfig.expectCt?.enabled) {
let expectCtValue = `max-age=${mergedConfig.expectCt.maxAge || 86400}`;
if (mergedConfig.expectCt.enforce) {
expectCtValue += ', enforce';
}
if (mergedConfig.expectCt.reportUri) {
expectCtValue += `, report-uri="${mergedConfig.expectCt.reportUri}"`;
}
headers.set('Expect-CT', expectCtValue);
}
// Apply Cross-Origin-Embedder-Policy
if (mergedConfig.crossOriginEmbedderPolicy?.enabled) {
headers.set('Cross-Origin-Embedder-Policy', mergedConfig.crossOriginEmbedderPolicy.policy || 'unsafe-none');
}
// Apply Cross-Origin-Opener-Policy
if (mergedConfig.crossOriginOpenerPolicy?.enabled) {
headers.set('Cross-Origin-Opener-Policy', mergedConfig.crossOriginOpenerPolicy.policy || 'same-origin-allow-popups');
}
// Apply Cross-Origin-Resource-Policy
if (mergedConfig.crossOriginResourcePolicy?.enabled) {
headers.set('Cross-Origin-Resource-Policy', mergedConfig.crossOriginResourcePolicy.policy || 'same-site');
}
return response;
};
}
export function sanitizeInput(input: string): string {
if (typeof input !== 'string') {
throw new Error('Input must be a string');
}
return input
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''')
.replace(/\//g, '/');
}
export interface CookieOptions {
httpOnly?: boolean;
secure?: boolean;
sameSite?: 'strict' | 'lax' | 'none';
maxAge?: number;
path?: string;
domain?: string;
expires?: Date;
[key: string]: unknown;
}
export function applyCookieDefaults(options: CookieOptions = {}): CookieOptions {
return {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
...options,
};
}
// Utility function to generate nonce for CSP
export function generateNonce(): string {
const buffer = new Uint8Array(16);
if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
crypto.getRandomValues(buffer);
} else {
// Fallback for environments without crypto.getRandomValues
for (let i = 0; i < buffer.length; i++) {
buffer[i] = Math.floor(Math.random() * 256);
}
}
return btoa(String.fromCharCode(...buffer));
}
// Preset configurations for common use cases
export const presets = {
strict: {
contentSecurityPolicy: {
enabled: true,
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'"],
imgSrc: ["'self'", 'data:'],
connectSrc: ["'self'"],
fontSrc: ["'self'"],
objectSrc: ["'none'"],
mediaSrc: ["'none'"],
frameSrc: ["'none'"],
frameAncestors: ["'none'"],
baseUri: ["'self'"],
upgradeInsecureRequests: true,
},
},
xFrameOptions: { enabled: true, option: 'DENY' },
hsts: { enabled: true, maxAge: 31536000, includeSubDomains: true, preload: true },
xContentTypeOptions: { enabled: true },
referrerPolicy: { enabled: true, policy: 'strict-origin-when-cross-origin' },
} as Partial<WebSecureConfig>,
moderate: {
contentSecurityPolicy: {
enabled: true,
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", 'data:', 'https:'],
connectSrc: ["'self'"],
fontSrc: ["'self'", 'https:', 'data:'],
objectSrc: ["'none'"],
upgradeInsecureRequests: true,
},
},
xFrameOptions: { enabled: true, option: 'SAMEORIGIN' },
hsts: { enabled: true, maxAge: 31536000 },
xContentTypeOptions: { enabled: true },
referrerPolicy: { enabled: true, policy: 'strict-origin-when-cross-origin' },
} as Partial<WebSecureConfig>,
};
const websecureEz = {
createSecureMiddleware,
sanitizeInput,
applyCookieDefaults,
generateNonce,
presets,
};
export default websecureEz;