javascript
examples
examples.js⚡javascript
/**
* 21.2 Secure Coding Practices - Examples
*
* Demonstrating secure coding patterns and best practices
*/
// ================================================
// 1. INPUT VALIDATION
// ================================================
/**
* Schema-Based Validator
* Comprehensive input validation using schemas
*/
class SchemaValidator {
constructor(schema) {
this.schema = schema;
}
validate(data) {
const errors = [];
const validated = {};
for (const [field, rules] of Object.entries(this.schema)) {
const value = data[field];
// Check required
if (
rules.required &&
(value === undefined || value === null || value === '')
) {
errors.push({ field, message: `${field} is required` });
continue;
}
// Skip validation if not required and not provided
if (value === undefined || value === null) {
if (rules.default !== undefined) {
validated[field] = rules.default;
}
continue;
}
// Type validation
if (rules.type && !this.validateType(value, rules.type)) {
errors.push({
field,
message: `${field} must be of type ${rules.type}`,
});
continue;
}
// String validations
if (rules.type === 'string') {
if (rules.minLength && value.length < rules.minLength) {
errors.push({
field,
message: `${field} must be at least ${rules.minLength} characters`,
});
}
if (rules.maxLength && value.length > rules.maxLength) {
errors.push({
field,
message: `${field} must be at most ${rules.maxLength} characters`,
});
}
if (rules.pattern && !rules.pattern.test(value)) {
errors.push({ field, message: `${field} format is invalid` });
}
}
// Number validations
if (rules.type === 'number') {
if (rules.min !== undefined && value < rules.min) {
errors.push({
field,
message: `${field} must be at least ${rules.min}`,
});
}
if (rules.max !== undefined && value > rules.max) {
errors.push({
field,
message: `${field} must be at most ${rules.max}`,
});
}
if (rules.integer && !Number.isInteger(value)) {
errors.push({ field, message: `${field} must be an integer` });
}
}
// Array validations
if (rules.type === 'array') {
if (rules.minItems && value.length < rules.minItems) {
errors.push({
field,
message: `${field} must have at least ${rules.minItems} items`,
});
}
if (rules.maxItems && value.length > rules.maxItems) {
errors.push({
field,
message: `${field} must have at most ${rules.maxItems} items`,
});
}
if (rules.itemType) {
for (let i = 0; i < value.length; i++) {
if (!this.validateType(value[i], rules.itemType)) {
errors.push({
field,
message: `${field}[${i}] must be of type ${rules.itemType}`,
});
}
}
}
}
// Enum validation
if (rules.enum && !rules.enum.includes(value)) {
errors.push({
field,
message: `${field} must be one of: ${rules.enum.join(', ')}`,
});
}
// Custom validator
if (rules.validate) {
const customResult = rules.validate(value, data);
if (customResult !== true) {
errors.push({
field,
message: customResult || `${field} is invalid`,
});
}
}
// Sanitize and store
validated[field] = rules.sanitize ? rules.sanitize(value) : value;
}
return {
valid: errors.length === 0,
errors,
data: validated,
};
}
validateType(value, type) {
switch (type) {
case 'string':
return typeof value === 'string';
case 'number':
return typeof value === 'number' && !isNaN(value);
case 'boolean':
return typeof value === 'boolean';
case 'array':
return Array.isArray(value);
case 'object':
return (
typeof value === 'object' && value !== null && !Array.isArray(value)
);
case 'email':
return (
typeof value === 'string' && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
);
case 'url':
try {
new URL(value);
return true;
} catch {
return false;
}
default:
return true;
}
}
}
// Example usage
const userSchema = {
username: {
type: 'string',
required: true,
minLength: 3,
maxLength: 20,
pattern: /^[a-zA-Z0-9_]+$/,
sanitize: (v) => v.toLowerCase().trim(),
},
email: {
type: 'email',
required: true,
sanitize: (v) => v.toLowerCase().trim(),
},
age: {
type: 'number',
min: 13,
max: 120,
integer: true,
},
role: {
type: 'string',
enum: ['user', 'admin', 'moderator'],
default: 'user',
},
password: {
type: 'string',
required: true,
minLength: 8,
validate: (value) => {
if (!/[A-Z]/.test(value)) return 'Password must contain uppercase letter';
if (!/[a-z]/.test(value)) return 'Password must contain lowercase letter';
if (!/[0-9]/.test(value)) return 'Password must contain number';
return true;
},
},
};
const validator = new SchemaValidator(userSchema);
console.log('Schema Validator Examples:');
console.log('-'.repeat(50));
// Valid data
const validResult = validator.validate({
username: 'JohnDoe',
email: 'john@example.com',
age: 25,
password: 'SecurePass123',
});
console.log('Valid data result:', validResult);
// Invalid data
const invalidResult = validator.validate({
username: 'ab',
email: 'invalid-email',
age: 5,
password: 'weak',
});
console.log('Invalid data result:', invalidResult);
// ================================================
// 2. OUTPUT ENCODING
// ================================================
/**
* Context-Aware Output Encoder
* Encodes output based on the context where it will be used
*/
class OutputEncoder {
// HTML body context - encode HTML entities
static html(str) {
if (typeof str !== 'string') return '';
const htmlEntities = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
'/': '/',
};
return str.replace(/[&<>"'/]/g, (char) => htmlEntities[char]);
}
// HTML attribute context
static htmlAttr(str) {
if (typeof str !== 'string') return '';
// More aggressive encoding for attributes
return str.replace(/[&<>"'`=\/]/g, (char) => {
return '&#x' + char.charCodeAt(0).toString(16) + ';';
});
}
// JavaScript string context
static js(str) {
if (typeof str !== 'string') return '';
const jsEscapes = {
'\\': '\\\\',
"'": "\\'",
'"': '\\"',
'\n': '\\n',
'\r': '\\r',
'\t': '\\t',
'<': '\\u003c',
'>': '\\u003e',
'/': '\\/',
'\u2028': '\\u2028', // Line separator
'\u2029': '\\u2029', // Paragraph separator
};
return str.replace(
/[\\'"<>\n\r\t\/\u2028\u2029]/g,
(char) => jsEscapes[char]
);
}
// CSS context
static css(str) {
if (typeof str !== 'string') return '';
// Escape non-alphanumeric characters
return str.replace(/[^a-zA-Z0-9]/g, (char) => {
return '\\' + char.charCodeAt(0).toString(16) + ' ';
});
}
// URL component encoding
static url(str) {
if (typeof str !== 'string') return '';
return encodeURIComponent(str);
}
// Full URL encoding (preserves structure)
static fullUrl(str) {
if (typeof str !== 'string') return '';
return encodeURI(str);
}
// JSON encoding with extra safety
static json(data) {
const jsonStr = JSON.stringify(data);
// Escape characters that could break out of JSON in HTML
return jsonStr
.replace(/</g, '\\u003c')
.replace(/>/g, '\\u003e')
.replace(/&/g, '\\u0026');
}
}
console.log('\n' + '='.repeat(50));
console.log('Output Encoder Examples:');
console.log('-'.repeat(50));
const dangerous = '<script>alert("XSS")</script>';
console.log('Original:', dangerous);
console.log('HTML encoded:', OutputEncoder.html(dangerous));
console.log('HTML attr encoded:', OutputEncoder.htmlAttr(dangerous));
console.log('JS encoded:', OutputEncoder.js(dangerous));
console.log('URL encoded:', OutputEncoder.url(dangerous));
// ================================================
// 3. SECURE PASSWORD HANDLING
// ================================================
/**
* Password Security Manager
* Handles password strength checking and secure comparison
*/
class PasswordSecurity {
static commonPasswords = new Set([
'password',
'123456',
'12345678',
'qwerty',
'abc123',
'password123',
'admin',
'letmein',
'welcome',
'monkey',
]);
static checkStrength(password) {
const checks = {
length: password.length >= 12,
uppercase: /[A-Z]/.test(password),
lowercase: /[a-z]/.test(password),
numbers: /\d/.test(password),
special: /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password),
noCommon: !this.isCommonPassword(password),
noRepeating: !/(.)\1{2,}/.test(password), // No 3+ repeating chars
noSequential: !this.hasSequentialChars(password),
};
const score = Object.values(checks).filter(Boolean).length;
let strength = 'weak';
if (score >= 7) strength = 'strong';
else if (score >= 5) strength = 'medium';
return {
strength,
score,
maxScore: 8,
checks,
suggestions: this.getSuggestions(checks),
};
}
static isCommonPassword(password) {
return this.commonPasswords.has(password.toLowerCase());
}
static hasSequentialChars(password) {
const sequences = [
'abcdefghijklmnopqrstuvwxyz',
'zyxwvutsrqponmlkjihgfedcba',
'0123456789',
'9876543210',
'qwertyuiop',
'asdfghjkl',
'zxcvbnm',
];
const lower = password.toLowerCase();
for (const seq of sequences) {
for (let i = 0; i < seq.length - 3; i++) {
if (lower.includes(seq.substring(i, i + 4))) {
return true;
}
}
}
return false;
}
static getSuggestions(checks) {
const suggestions = [];
if (!checks.length) suggestions.push('Use at least 12 characters');
if (!checks.uppercase) suggestions.push('Add uppercase letters');
if (!checks.lowercase) suggestions.push('Add lowercase letters');
if (!checks.numbers) suggestions.push('Add numbers');
if (!checks.special) suggestions.push('Add special characters');
if (!checks.noCommon) suggestions.push('Avoid common passwords');
if (!checks.noRepeating) suggestions.push('Avoid repeating characters');
if (!checks.noSequential) suggestions.push('Avoid sequential characters');
return suggestions;
}
// Timing-safe comparison to prevent timing attacks
static secureCompare(a, b) {
if (typeof a !== 'string' || typeof b !== 'string') {
return false;
}
// Ensure same length comparison
const aBytes = Buffer.from(a, 'utf8');
const bBytes = Buffer.from(b, 'utf8');
if (aBytes.length !== bBytes.length) {
// Compare against dummy to maintain constant time
const dummy = Buffer.alloc(aBytes.length);
this.timingSafeEqual(aBytes, dummy);
return false;
}
return this.timingSafeEqual(aBytes, bBytes);
}
static timingSafeEqual(a, b) {
let result = 0;
for (let i = 0; i < a.length; i++) {
result |= a[i] ^ b[i];
}
return result === 0;
}
// Generate secure random password
static generatePassword(length = 16) {
const charset =
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*';
let password = '';
// Ensure at least one of each required type
password += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'[Math.floor(Math.random() * 26)];
password += 'abcdefghijklmnopqrstuvwxyz'[Math.floor(Math.random() * 26)];
password += '0123456789'[Math.floor(Math.random() * 10)];
password += '!@#$%^&*'[Math.floor(Math.random() * 8)];
// Fill remaining length
for (let i = password.length; i < length; i++) {
password += charset[Math.floor(Math.random() * charset.length)];
}
// Shuffle password
return password
.split('')
.sort(() => Math.random() - 0.5)
.join('');
}
}
console.log('\n' + '='.repeat(50));
console.log('Password Security Examples:');
console.log('-'.repeat(50));
console.log('Weak password:', PasswordSecurity.checkStrength('password123'));
console.log(
'Strong password:',
PasswordSecurity.checkStrength('MyS3cur3!Pass#2024')
);
console.log('Generated password:', PasswordSecurity.generatePassword(16));
// ================================================
// 4. SECURE ERROR HANDLING
// ================================================
/**
* Secure Error Handler
* Logs detailed errors server-side, returns safe messages to client
*/
class SecureErrorHandler {
constructor(options = {}) {
this.isProduction =
options.isProduction ?? process.env.NODE_ENV === 'production';
this.logFunction = options.logFunction || console.error;
this.errorIdGenerator =
options.errorIdGenerator ||
(() =>
'ERR-' +
Date.now().toString(36) +
'-' +
Math.random().toString(36).substr(2, 9));
}
// Map of safe error messages for different error types
safeMessages = {
ValidationError: 'Invalid input provided',
AuthenticationError: 'Authentication failed',
AuthorizationError: 'Access denied',
NotFoundError: 'Resource not found',
RateLimitError: 'Too many requests',
DatabaseError: 'Service temporarily unavailable',
NetworkError: 'Service temporarily unavailable',
default: 'An unexpected error occurred',
};
handle(error, context = {}) {
const errorId = this.errorIdGenerator();
const timestamp = new Date().toISOString();
// Log detailed information server-side
this.logFunction({
errorId,
timestamp,
error: {
name: error.name,
message: error.message,
stack: error.stack,
},
context: {
userId: context.userId,
requestId: context.requestId,
path: context.path,
method: context.method,
},
});
// Return safe response
return this.createSafeResponse(error, errorId);
}
createSafeResponse(error, errorId) {
const safeMessage =
this.safeMessages[error.name] || this.safeMessages.default;
const response = {
success: false,
error: safeMessage,
errorId,
};
// In development, include more details
if (!this.isProduction) {
response.debug = {
name: error.name,
message: error.message,
};
}
return response;
}
// Create custom error types
static createError(name, statusCode) {
return class extends Error {
constructor(message, details = {}) {
super(message);
this.name = name;
this.statusCode = statusCode;
this.details = details;
}
};
}
}
// Create custom error types
const ValidationError = SecureErrorHandler.createError('ValidationError', 400);
const AuthenticationError = SecureErrorHandler.createError(
'AuthenticationError',
401
);
const AuthorizationError = SecureErrorHandler.createError(
'AuthorizationError',
403
);
const NotFoundError = SecureErrorHandler.createError('NotFoundError', 404);
console.log('\n' + '='.repeat(50));
console.log('Secure Error Handler Examples:');
console.log('-'.repeat(50));
const errorHandler = new SecureErrorHandler({ isProduction: true });
// Database error - should not expose details
const dbError = new Error(
'ECONNREFUSED: Connection refused to postgres://user:pass@localhost:5432/db'
);
dbError.name = 'DatabaseError';
console.log(
'Database error (production):',
errorHandler.handle(dbError, { userId: 123, path: '/api/users' })
);
// Validation error
const validationError = new ValidationError('Email format is invalid');
console.log('Validation error:', errorHandler.handle(validationError));
// ================================================
// 5. SECURE CONFIGURATION MANAGEMENT
// ================================================
/**
* Secure Configuration Manager
* Handles configuration with environment variables and validation
*/
class SecureConfig {
constructor(schema) {
this.schema = schema;
this.config = {};
this.sensitiveKeys = new Set();
this.loaded = false;
}
load() {
for (const [key, definition] of Object.entries(this.schema)) {
const envVar = definition.env || key.toUpperCase().replace(/\./g, '_');
let value = process.env[envVar] || definition.default;
// Mark sensitive keys
if (definition.sensitive) {
this.sensitiveKeys.add(key);
}
// Required check
if (definition.required && (value === undefined || value === null)) {
throw new Error(
`Required configuration missing: ${key} (env: ${envVar})`
);
}
// Type conversion
if (value !== undefined) {
value = this.convertType(value, definition.type);
}
// Validation
if (definition.validate && value !== undefined) {
const validationResult = definition.validate(value);
if (validationResult !== true) {
throw new Error(
`Configuration validation failed for ${key}: ${validationResult}`
);
}
}
this.config[key] = value;
}
this.loaded = true;
return this;
}
convertType(value, type) {
switch (type) {
case 'number':
const num = Number(value);
if (isNaN(num)) throw new Error(`Cannot convert to number: ${value}`);
return num;
case 'boolean':
return value === 'true' || value === '1' || value === true;
case 'array':
return typeof value === 'string'
? value.split(',').map((v) => v.trim())
: value;
case 'json':
return typeof value === 'string' ? JSON.parse(value) : value;
default:
return value;
}
}
get(key) {
if (!this.loaded) {
throw new Error('Configuration not loaded. Call load() first.');
}
return this.config[key];
}
// Get all config, masking sensitive values
getAll(maskSensitive = true) {
if (!this.loaded) {
throw new Error('Configuration not loaded. Call load() first.');
}
const result = { ...this.config };
if (maskSensitive) {
for (const key of this.sensitiveKeys) {
if (result[key]) {
result[key] = '***MASKED***';
}
}
}
return result;
}
// Validate all required configurations
validate() {
const issues = [];
for (const [key, definition] of Object.entries(this.schema)) {
const value = this.config[key];
if (
definition.required &&
(value === undefined || value === null || value === '')
) {
issues.push(`Missing required: ${key}`);
}
}
return {
valid: issues.length === 0,
issues,
};
}
}
// Example configuration schema
const configSchema = {
'app.port': {
env: 'PORT',
type: 'number',
default: 3000,
validate: (v) => (v > 0 && v < 65536 ? true : 'Port must be 1-65535'),
},
'app.environment': {
env: 'NODE_ENV',
type: 'string',
default: 'development',
},
'db.host': {
env: 'DB_HOST',
type: 'string',
default: 'localhost',
},
'db.password': {
env: 'DB_PASSWORD',
type: 'string',
sensitive: true,
required: false, // Would be true in production
},
'jwt.secret': {
env: 'JWT_SECRET',
type: 'string',
sensitive: true,
required: false,
},
'cors.origins': {
env: 'CORS_ORIGINS',
type: 'array',
default: ['http://localhost:3000'],
},
};
console.log('\n' + '='.repeat(50));
console.log('Secure Configuration Examples:');
console.log('-'.repeat(50));
// Set test environment variables
process.env.PORT = '8080';
process.env.DB_PASSWORD = 'super-secret-password';
const config = new SecureConfig(configSchema).load();
console.log('All config (masked):', config.getAll(true));
console.log('Port value:', config.get('app.port'));
// ================================================
// 6. SECURE API REQUEST HANDLER
// ================================================
/**
* Secure API Handler
* Combines validation, sanitization, and error handling
*/
class SecureAPIHandler {
constructor(options = {}) {
this.rateLimiter = new Map();
this.maxRequests = options.maxRequests || 100;
this.windowMs = options.windowMs || 60000;
this.errorHandler = new SecureErrorHandler({
isProduction: options.isProduction,
});
}
// Rate limiting check
checkRateLimit(clientId) {
const now = Date.now();
const clientData = this.rateLimiter.get(clientId) || {
count: 0,
resetTime: now + this.windowMs,
};
// Reset if window expired
if (now > clientData.resetTime) {
clientData.count = 0;
clientData.resetTime = now + this.windowMs;
}
clientData.count++;
this.rateLimiter.set(clientId, clientData);
if (clientData.count > this.maxRequests) {
const error = new Error('Rate limit exceeded');
error.name = 'RateLimitError';
throw error;
}
return {
remaining: this.maxRequests - clientData.count,
resetTime: clientData.resetTime,
};
}
// Sanitize request data
sanitizeRequest(data) {
if (typeof data === 'string') {
// Remove null bytes
data = data.replace(/\0/g, '');
// Trim whitespace
data = data.trim();
// Normalize unicode
data = data.normalize('NFC');
} else if (typeof data === 'object' && data !== null) {
for (const key of Object.keys(data)) {
data[key] = this.sanitizeRequest(data[key]);
}
}
return data;
}
// Create handler wrapper
createHandler(schema, handler) {
const validator = new SchemaValidator(schema);
return async (req) => {
try {
// Rate limiting
const rateLimitInfo = this.checkRateLimit(req.clientId || req.ip);
// Sanitize
const sanitizedData = this.sanitizeRequest(req.body);
// Validate
const validation = validator.validate(sanitizedData);
if (!validation.valid) {
const error = new Error(validation.errors[0].message);
error.name = 'ValidationError';
throw error;
}
// Execute handler
const result = await handler(validation.data, req);
return {
success: true,
data: result,
rateLimit: rateLimitInfo,
};
} catch (error) {
return this.errorHandler.handle(error, {
userId: req.userId,
path: req.path,
method: req.method,
});
}
};
}
}
console.log('\n' + '='.repeat(50));
console.log('Secure API Handler Examples:');
console.log('-'.repeat(50));
const apiHandler = new SecureAPIHandler({ isProduction: false });
const createUserHandler = apiHandler.createHandler(
{
username: { type: 'string', required: true, minLength: 3 },
email: { type: 'email', required: true },
},
async (data) => {
// Simulated user creation
return { id: 1, ...data, created: new Date().toISOString() };
}
);
// Test the handler
(async () => {
const validRequest = {
clientId: 'client-1',
body: { username: 'john_doe', email: 'john@example.com' },
path: '/api/users',
method: 'POST',
};
console.log('Valid request result:', await createUserHandler(validRequest));
const invalidRequest = {
clientId: 'client-2',
body: { username: 'jo', email: 'invalid' },
path: '/api/users',
method: 'POST',
};
console.log(
'Invalid request result:',
await createUserHandler(invalidRequest)
);
})();
// ================================================
// 7. CONTENT SECURITY POLICY BUILDER
// ================================================
/**
* CSP Builder
* Creates Content Security Policy headers
*/
class CSPBuilder {
constructor() {
this.directives = {};
}
// Add directive
directive(name, ...values) {
if (!this.directives[name]) {
this.directives[name] = new Set();
}
values.forEach((v) => this.directives[name].add(v));
return this;
}
// Common directive helpers
defaultSrc(...sources) {
return this.directive('default-src', ...sources);
}
scriptSrc(...sources) {
return this.directive('script-src', ...sources);
}
styleSrc(...sources) {
return this.directive('style-src', ...sources);
}
imgSrc(...sources) {
return this.directive('img-src', ...sources);
}
connectSrc(...sources) {
return this.directive('connect-src', ...sources);
}
fontSrc(...sources) {
return this.directive('font-src', ...sources);
}
objectSrc(...sources) {
return this.directive('object-src', ...sources);
}
mediaSrc(...sources) {
return this.directive('media-src', ...sources);
}
frameSrc(...sources) {
return this.directive('frame-src', ...sources);
}
// Special directives
upgradeInsecureRequests() {
return this.directive('upgrade-insecure-requests');
}
blockAllMixedContent() {
return this.directive('block-all-mixed-content');
}
// Generate nonce
static generateNonce() {
return Buffer.from(Math.random().toString() + Date.now().toString())
.toString('base64')
.slice(0, 16);
}
// Build the CSP string
build() {
return Object.entries(this.directives)
.map(([directive, values]) => {
const valueStr = Array.from(values).join(' ');
return valueStr ? `${directive} ${valueStr}` : directive;
})
.join('; ');
}
// Common presets
static strict() {
return new CSPBuilder()
.defaultSrc("'self'")
.scriptSrc("'self'")
.styleSrc("'self'")
.imgSrc("'self'", 'data:', 'https:')
.connectSrc("'self'")
.fontSrc("'self'")
.objectSrc("'none'")
.frameSrc("'none'")
.upgradeInsecureRequests();
}
static moderate() {
return new CSPBuilder()
.defaultSrc("'self'")
.scriptSrc("'self'", "'unsafe-inline'")
.styleSrc("'self'", "'unsafe-inline'")
.imgSrc("'self'", 'data:', 'https:')
.connectSrc("'self'", 'https:')
.fontSrc("'self'", 'https:')
.objectSrc("'none'");
}
}
console.log('\n' + '='.repeat(50));
console.log('CSP Builder Examples:');
console.log('-'.repeat(50));
const strictCSP = CSPBuilder.strict();
console.log('Strict CSP:', strictCSP.build());
const customCSP = new CSPBuilder()
.defaultSrc("'self'")
.scriptSrc("'self'", 'https://cdn.example.com')
.styleSrc("'self'", "'unsafe-inline'")
.imgSrc("'self'", 'https:', 'data:')
.connectSrc("'self'", 'https://api.example.com')
.objectSrc("'none'")
.upgradeInsecureRequests();
console.log('Custom CSP:', customCSP.build());
// ================================================
// 8. SECURE FILE UPLOAD HANDLER
// ================================================
/**
* Secure File Upload Validator
* Validates file uploads for security
*/
class SecureFileUpload {
constructor(options = {}) {
this.allowedTypes = options.allowedTypes || [
'image/jpeg',
'image/png',
'image/gif',
];
this.maxSize = options.maxSize || 5 * 1024 * 1024; // 5MB
this.allowedExtensions = options.allowedExtensions || [
'.jpg',
'.jpeg',
'.png',
'.gif',
];
}
// Magic bytes for file type detection
static magicBytes = {
'image/jpeg': [[0xff, 0xd8, 0xff]],
'image/png': [[0x89, 0x50, 0x4e, 0x47]],
'image/gif': [[0x47, 0x49, 0x46, 0x38]],
'application/pdf': [[0x25, 0x50, 0x44, 0x46]],
};
validate(file) {
const errors = [];
// Check file exists
if (!file || !file.buffer) {
return { valid: false, errors: ['No file provided'] };
}
// Check size
if (file.size > this.maxSize) {
errors.push(
`File size exceeds maximum of ${this.maxSize / 1024 / 1024}MB`
);
}
// Check extension
const ext = this.getExtension(file.name);
if (!this.allowedExtensions.includes(ext.toLowerCase())) {
errors.push(`File extension not allowed: ${ext}`);
}
// Check MIME type
if (!this.allowedTypes.includes(file.mimeType)) {
errors.push(`File type not allowed: ${file.mimeType}`);
}
// Verify magic bytes match declared type
const detectedType = this.detectType(file.buffer);
if (detectedType && detectedType !== file.mimeType) {
errors.push(`File content doesn't match declared type`);
}
// Check for dangerous content
if (this.hasExecutableContent(file.buffer)) {
errors.push('File contains potentially dangerous content');
}
return {
valid: errors.length === 0,
errors,
sanitizedName: this.sanitizeFilename(file.name),
};
}
getExtension(filename) {
const match = filename.match(/\.[^.]+$/);
return match ? match[0] : '';
}
detectType(buffer) {
for (const [type, signatures] of Object.entries(
SecureFileUpload.magicBytes
)) {
for (const signature of signatures) {
if (this.matchesSignature(buffer, signature)) {
return type;
}
}
}
return null;
}
matchesSignature(buffer, signature) {
for (let i = 0; i < signature.length; i++) {
if (buffer[i] !== signature[i]) {
return false;
}
}
return true;
}
hasExecutableContent(buffer) {
const content = buffer.toString('utf8', 0, Math.min(buffer.length, 1000));
// Check for script tags, PHP code, etc.
const dangerousPatterns = [/<script/i, /<\?php/i, /<%/, /javascript:/i];
return dangerousPatterns.some((pattern) => pattern.test(content));
}
sanitizeFilename(filename) {
// Remove path components
filename = filename.replace(/^.*[\\\/]/, '');
// Remove dangerous characters
filename = filename.replace(/[^a-zA-Z0-9._-]/g, '_');
// Limit length
if (filename.length > 100) {
const ext = this.getExtension(filename);
filename = filename.slice(0, 100 - ext.length) + ext;
}
return filename;
}
}
console.log('\n' + '='.repeat(50));
console.log('Secure File Upload Examples:');
console.log('-'.repeat(50));
const uploader = new SecureFileUpload();
// Simulate file validation
const validFile = {
name: 'photo.jpg',
mimeType: 'image/jpeg',
size: 1024 * 100, // 100KB
buffer: Buffer.from([0xff, 0xd8, 0xff, 0xe0]), // JPEG magic bytes
};
console.log('Valid file:', uploader.validate(validFile));
const invalidFile = {
name: '../../../etc/passwd.jpg',
mimeType: 'image/jpeg',
size: 10 * 1024 * 1024, // 10MB
buffer: Buffer.from([0x25, 0x50, 0x44, 0x46]), // PDF magic bytes
};
console.log('Invalid file:', uploader.validate(invalidFile));
// ================================================
// EXPORTS
// ================================================
module.exports = {
SchemaValidator,
OutputEncoder,
PasswordSecurity,
SecureErrorHandler,
SecureConfig,
SecureAPIHandler,
CSPBuilder,
SecureFileUpload,
// Custom errors
ValidationError,
AuthenticationError,
AuthorizationError,
NotFoundError,
};