/**
* Security configuration and constants for MCP WordPress
*/
import { randomBytes } from "crypto";
import * as path from "path";
import { LoggerFactory } from "@/utils/logger.js";
const logger = LoggerFactory.security();
export const SecurityConfig = {
// Rate limiting
rateLimiting: {
default: {
windowMs: 60 * 1000, // 1 minute
maxRequests: 60,
},
authentication: {
windowMs: 5 * 60 * 1000, // 5 minutes
maxAttempts: 5,
},
upload: {
windowMs: 60 * 1000, // 1 minute
maxRequests: 10,
},
},
// File upload restrictions
fileUpload: {
maxSizeMB: 10,
allowedMimeTypes: [
"image/jpeg",
"image/png",
"image/gif",
"image/webp",
"image/svg+xml",
"application/pdf",
"application/msword",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/vnd.ms-excel",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"text/plain",
"text/csv",
],
// Dangerous file extensions to block
blockedExtensions: [
".exe",
".bat",
".cmd",
".com",
".pif",
".scr",
".vbs",
".js",
".jar",
".zip",
".rar",
".tar",
".php",
".php3",
".php4",
".php5",
".phtml",
".sh",
".bash",
".zsh",
".fish",
".ps1",
".psm1",
],
},
// Input validation
validation: {
maxStringLength: 1000,
maxTitleLength: 200,
maxContentLength: 50000,
maxExcerptLength: 500,
maxUrlLength: 2048,
maxUsernameLength: 60,
minUsernameLength: 3,
maxPasswordLength: 128,
minPasswordLength: 8,
},
// Request timeouts (milliseconds)
timeouts: {
default: 30000, // 30 seconds
upload: 600000, // 10 minutes
auth: 10000, // 10 seconds
},
// Security headers
headers: {
"X-Content-Type-Options": "nosniff",
"X-Frame-Options": "DENY",
"X-XSS-Protection": "1; mode=block",
"Strict-Transport-Security": "max-age=31536000; includeSubDomains",
"Content-Security-Policy": "default-src 'self'",
},
// Error messages (generic to avoid information disclosure)
errorMessages: {
authentication: "Authentication failed. Please check your credentials.",
authorization: "You do not have permission to perform this action.",
validation: "Invalid input provided.",
rateLimit: "Too many requests. Please try again later.",
serverError: "An error occurred processing your request.",
notFound: "The requested resource was not found.",
},
// Logging configuration
logging: {
// Fields to exclude from logs
excludeFields: [
"password",
"appPassword",
"app_password",
"token",
"secret",
"authorization",
"cookie",
"session",
"key",
"apiKey",
"api_key",
],
// Patterns to redact in log messages
redactPatterns: [
/password["\s:=]+["']?([^"'\s]+)["']?/gi,
/token["\s:=]+["']?([^"'\s]+)["']?/gi,
/secret["\s:=]+["']?([^"'\s]+)["']?/gi,
/key["\s:=]+["']?([^"'\s]+)["']?/gi,
],
},
// Cache configuration
cache: {
// Default cache settings
enabled: true,
maxSize: 1000, // Maximum number of cached entries
defaultTTL: 15 * 60 * 1000, // 15 minutes default TTL
enableLRU: true,
enableStats: true,
// TTL presets by data type (milliseconds)
ttlPresets: {
static: 4 * 60 * 60 * 1000, // 4 hours - site settings, user roles
semiStatic: 2 * 60 * 60 * 1000, // 2 hours - categories, tags, user profiles
dynamic: 15 * 60 * 1000, // 15 minutes - posts, pages, comments
session: 30 * 60 * 1000, // 30 minutes - authentication, current user
realtime: 60 * 1000, // 1 minute - real-time data
},
// Cache-Control headers by data type
cacheHeaders: {
static: "public, max-age=14400", // 4 hours
semiStatic: "public, max-age=7200", // 2 hours
dynamic: "public, max-age=900", // 15 minutes
session: "private, max-age=1800", // 30 minutes
realtime: "public, max-age=60", // 1 minute
},
// Invalidation settings
invalidation: {
enabled: true,
batchSize: 100, // Max events to process in one batch
queueTimeout: 5000, // Max time to wait before processing queue (ms)
enableCascading: true, // Allow cascading invalidations
},
// Memory management
cleanup: {
interval: 60 * 1000, // Cleanup interval in milliseconds (1 minute)
maxMemoryMB: 50, // Maximum memory usage for cache
evictionThreshold: 0.8, // Start evicting when 80% full
},
},
};
/**
* Security utility functions
*/
export class SecurityUtils {
/**
* Redact sensitive information from objects
*/
static redactSensitiveData(obj: unknown): unknown {
if (typeof obj !== "object" || obj === null) {
return obj;
}
const working: Record<string, unknown> | unknown[] = Array.isArray(obj)
? [...obj]
: { ...(obj as Record<string, unknown>) };
if (Array.isArray(working)) {
return working.map((val) => (typeof val === "object" ? SecurityUtils.redactSensitiveData(val) : val));
}
for (const key in working) {
if (SecurityConfig.logging.excludeFields.some((field) => key.toLowerCase().includes(field.toLowerCase()))) {
working[key] = "[REDACTED]";
} else if (typeof working[key] === "object" && working[key] !== null) {
working[key] = SecurityUtils.redactSensitiveData(working[key]);
}
}
return working;
}
/**
* Redact sensitive patterns from strings
*/
static redactString(str: string): string {
let redacted = str;
for (const pattern of SecurityConfig.logging.redactPatterns) {
redacted = redacted.replace(pattern, (match, value) => {
return match.replace(value, "[REDACTED]");
});
}
return redacted;
}
/**
* Generate secure random strings
*/
static generateSecureToken(length: number = 32): string {
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
const array = new Uint8Array(length);
if (typeof crypto !== "undefined" && crypto.getRandomValues) {
crypto.getRandomValues(array);
} else {
// Fallback for Node.js
const buffer = randomBytes(length);
array.set(buffer);
}
return Array.from(array, (byte) => chars[byte % chars.length]).join("");
}
/**
* Check if a file extension is allowed
*/
static isFileExtensionAllowed(filename: string): boolean {
const ext = path.extname(filename).toLowerCase();
return !SecurityConfig.fileUpload.blockedExtensions.includes(ext);
}
/**
* Sanitize log output
*/
static sanitizeForLog(data: unknown): unknown {
if (typeof data === "string") {
return SecurityUtils.redactString(data);
}
if (typeof data === "object") {
return SecurityUtils.redactSensitiveData(data);
}
return data;
}
}
/**
* Secure error handler that prevents information disclosure
*/
export function createSecureError(
error: unknown,
fallbackMessage: string = SecurityConfig.errorMessages.serverError,
): Error {
// Log the actual error for debugging (with sanitization)
if (process.env.NODE_ENV !== "production") {
logger.error("Secure Error", { error: SecurityUtils.sanitizeForLog(error) });
}
// Return generic error to prevent information disclosure
const secureError = new Error(fallbackMessage);
// Preserve error code if it's safe
if (
error &&
typeof (error as { code?: unknown }).code === "string" &&
!(error as { code: string }).code.includes("_")
) {
(secureError as unknown as Record<string, unknown>).code = (error as { code: string }).code;
}
return secureError;
}
/**
* Environment-specific security settings
*/
export function getEnvironmentSecurity(): {
strictMode: boolean;
verboseErrors: boolean;
enforceHttps: boolean;
} {
const isProduction = process.env.NODE_ENV === "production";
return {
strictMode: isProduction,
verboseErrors: !isProduction,
enforceHttps: isProduction,
};
}