From e4778135558969ca91b22e25df00093847bbedd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=E1=BA=A1nh=20T=C6=B0=E1=BB=9Dng=20Solo?= Date: Fri, 3 Oct 2025 21:27:53 +0700 Subject: [PATCH] feat: implement comprehensive email validation and sanitization for SSL operations --- apps/api/src/controllers/ssl.controller.ts | 76 +++++++++++++++++++++- apps/api/src/utils/acme.ts | 25 +++++++ 2 files changed, 100 insertions(+), 1 deletion(-) diff --git a/apps/api/src/controllers/ssl.controller.ts b/apps/api/src/controllers/ssl.controller.ts index 3479636..323955e 100644 --- a/apps/api/src/controllers/ssl.controller.ts +++ b/apps/api/src/controllers/ssl.controller.ts @@ -13,6 +13,68 @@ const execAsync = promisify(exec); const SSL_CERTS_PATH = '/etc/nginx/ssl'; +/** + * Validate email format to prevent injection attacks + */ +function validateEmail(email: string): boolean { + // RFC 5322 compliant email regex (simplified but secure) + const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; + + // Additional checks + if (email.length > 254) return false; // Max email length per RFC + if (email.includes('..')) return false; // No consecutive dots + if (email.startsWith('.') || email.endsWith('.')) return false; // No leading/trailing dots + + const parts = email.split('@'); + if (parts.length !== 2) return false; + + const [localPart, domain] = parts; + if (localPart.length > 64) return false; // Max local part length + if (domain.length > 253) return false; // Max domain length + + return emailRegex.test(email); +} + +/** + * Sanitize email input to prevent command injection + * Removes potentially dangerous characters while preserving valid email format + */ +function sanitizeEmail(email: string): string { + // Remove any characters that could be used for command injection + // Keep only characters valid in email addresses + return email.replace(/[;&|`$(){}[\]<>'"\\!*#?~\s]/g, ''); +} + +/** + * Validate and sanitize email with comprehensive security checks + */ +function secureEmail(email: string | undefined): string | undefined { + if (!email) return undefined; + + // Trim whitespace + email = email.trim(); + + // Check length before validation + if (email.length === 0 || email.length > 254) { + throw new Error('Invalid email format: length must be between 1 and 254 characters'); + } + + // Validate format + if (!validateEmail(email)) { + throw new Error('Invalid email format'); + } + + // Sanitize as additional security layer (defense in depth) + const sanitized = sanitizeEmail(email); + + // Verify sanitization didn't break the email + if (!validateEmail(sanitized)) { + throw new Error('Email contains invalid characters'); + } + + return sanitized; +} + /** * Get all SSL certificates */ @@ -124,6 +186,18 @@ export const issueAutoSSL = async (req: AuthRequest, res: Response): Promise { } } +/** + * Validate email format to prevent command injection + */ +function validateEmail(email: string): boolean { + const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; + return emailRegex.test(email); +} + +/** + * Sanitize input to prevent command injection + */ +function sanitizeInput(input: string): string { + // Remove potentially dangerous characters + return input.replace(/[;&|`$(){}[\]<>'"\\]/g, ''); +} + /** * Install acme.sh */ @@ -42,6 +58,15 @@ export async function installAcme(email?: string): Promise { try { logger.info('Installing acme.sh...'); + // Validate and sanitize email if provided + if (email) { + if (!validateEmail(email)) { + throw new Error('Invalid email format'); + } + // Additional sanitization as defense in depth + email = sanitizeInput(email); + } + const installCmd = email ? `curl https://get.acme.sh | sh -s email=${email}` : `curl https://get.acme.sh | sh`;