UNPKG

ctrlshiftleft

Version:

AI-powered toolkit for embedding QA and security testing into development workflows

293 lines (257 loc) 8.74 kB
/** * ctrl.shift.left QA Validation Middleware * * This middleware provides input validation and security checks for API routes * in various JavaScript frameworks including Next.js, Express, and others. */ type ValidationResult = { valid: boolean; message: string; securityIssues: string[]; }; type InputType = 'url' | 'email' | 'password' | 'text' | 'number' | 'json'; /** * Validate user input for common security risks * @param input User input to validate * @param inputType Type of input */ export function validateInput(input: string, inputType: InputType = 'text'): ValidationResult { // Basic validation patterns const patterns: Record<string, RegExp> = { url: /^(https?:\/\/)?([a-z0-9]([a-z0-9-]*[a-z0-9])?\.)+[a-z0-9]([a-z0-9-]*[a-z0-9])?([\/\w\.-]*)*\/?$/i, email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, password: /.{8,}/, // At least 8 characters number: /^-?\d+(\.\d+)?$/, }; // Check for common security issues const securityChecks: Record<string, Array<{pattern: RegExp, message: string}>> = { url: [ { pattern: /<|>|script|on\w+=/i, message: "URL contains potentially unsafe characters" }, { pattern: /javascript:/i, message: "URL contains JavaScript protocol" }, { pattern: /data:/i, message: "URL contains data protocol" }, ], email: [ { pattern: /<|>|script|on\w+=/i, message: "Email contains potentially unsafe characters" }, ], password: [ { pattern: /^(password|123456|admin|qwerty)/i, message: "Password is too common" }, ], text: [ { pattern: /<script|javascript:|on\w+=/i, message: "Text contains potentially unsafe code" }, { pattern: /\bdrop\s+table|\bdelete\s+from|\bupdate\s+\w+\s+set/i, message: "Text contains potential SQL commands" }, ], json: [ { pattern: /<script|javascript:|on\w+=/i, message: "JSON contains potentially unsafe code" }, { pattern: /__proto__|constructor|prototype/i, message: "JSON contains potential prototype pollution" } ], number: [ { pattern: /[^\d.-]/i, message: "Number contains non-numeric characters" } ] }; // Results const result: ValidationResult = { valid: false, message: '', securityIssues: [], }; // Handle empty input if (input === undefined || input === null || input === '') { result.message = 'Input is empty'; result.securityIssues.push('Empty input received'); return result; } // Convert to string if not already const inputStr = String(input); // Check basic pattern if available if (patterns[inputType] && !patterns[inputType].test(inputStr)) { result.message = `Invalid ${inputType} format`; result.securityIssues.push(result.message); return result; } // Check security issues if (securityChecks[inputType]) { for (const check of securityChecks[inputType]) { if (check.pattern.test(inputStr)) { result.securityIssues.push(check.message); } } } result.valid = result.securityIssues.length === 0; result.message = result.securityIssues.length > 0 ? result.securityIssues[0] : `Valid ${inputType}`; return result; } /** * Validate request body fields * @param body Request body object * @param fieldValidations Validation rules for each field */ export function validateRequestBody( body: Record<string, any>, fieldValidations: Record<string, { type: InputType, required?: boolean }> ): { isValid: boolean; errors: Record<string, string>; securityIssues: string[]; } { const result = { isValid: true, errors: {} as Record<string, string>, securityIssues: [] as string[] }; for (const [field, validation] of Object.entries(fieldValidations)) { const isRequired = validation.required !== false; // Default to true // Check if required field is missing if (isRequired && (body[field] === undefined || body[field] === null || body[field] === '')) { result.isValid = false; result.errors[field] = `${field} is required`; continue; } // Skip validation for optional empty fields if (!isRequired && (body[field] === undefined || body[field] === null || body[field] === '')) { continue; } // Validate the field const validationResult = validateInput(body[field], validation.type); if (!validationResult.valid) { result.isValid = false; result.errors[field] = validationResult.message; // Add security issues with field context validationResult.securityIssues.forEach(issue => { result.securityIssues.push(`${field}: ${issue}`); }); } } return result; } /** * Create Express-style middleware for input validation * @param validationRules Validation rules for request body fields */ export function createValidationMiddleware( validationRules: Record<string, { type: InputType, required?: boolean }> ) { return function validationMiddleware(req: any, res: any, next: () => void) { const validationResult = validateRequestBody(req.body, validationRules); if (!validationResult.isValid) { return res.status(400).json({ error: 'Validation failed', details: validationResult.errors, securityIssues: validationResult.securityIssues }); } // Add validation result to request for further processing if needed req.validationResult = validationResult; next(); }; } /** * Validate URL with security checks * @param url URL to validate */ export function validateUrl(url: string) { const result = validateInput(url, 'url'); return { isValid: result.valid, errorMessage: !result.valid ? result.message : '', hasSecurity: result.securityIssues?.length > 0, securityIssues: result.securityIssues || [] }; } /** * Validate email address with security checks * @param email Email to validate */ export function validateEmail(email: string) { const result = validateInput(email, 'email'); return { isValid: result.valid, errorMessage: !result.valid ? result.message : '', hasSecurity: result.securityIssues?.length > 0, securityIssues: result.securityIssues || [] }; } /** * Validate a password for security and strength * @param password Password to validate */ export function validatePassword(password: string) { const result = validateInput(password, 'password'); // Additional password strength checks const hasUppercase = /[A-Z]/.test(password); const hasLowercase = /[a-z]/.test(password); const hasNumber = /\d/.test(password); const hasSpecial = /[!@#$%^&*(),.?":{}|<>]/.test(password); const strength = [hasUppercase, hasLowercase, hasNumber, hasSpecial] .filter(Boolean).length; let strengthLabel = 'weak'; if (strength >= 3 && password.length >= 10) { strengthLabel = 'strong'; } else if (strength >= 2 && password.length >= 8) { strengthLabel = 'medium'; } return { isValid: result.valid, errorMessage: !result.valid ? result.message : '', hasSecurity: result.securityIssues?.length > 0, securityIssues: result.securityIssues || [], strength: strengthLabel, suggestions: strength < 3 ? [ !hasUppercase ? 'Add uppercase letters' : null, !hasLowercase ? 'Add lowercase letters' : null, !hasNumber ? 'Add numbers' : null, !hasSpecial ? 'Add special characters' : null, password.length < 10 ? 'Make it at least 10 characters long' : null ].filter(Boolean) : [] }; } /** * Validate JSON string or object for security issues * @param json JSON string or object to validate */ export function validateJson(json: string | object) { let jsonString: string; if (typeof json === 'object') { try { jsonString = JSON.stringify(json); } catch (error) { return { isValid: false, errorMessage: 'Invalid JSON object', hasSecurity: false, securityIssues: [] }; } } else { jsonString = json; // Try to parse to verify it's valid JSON try { JSON.parse(jsonString); } catch (error) { return { isValid: false, errorMessage: 'Invalid JSON string', hasSecurity: false, securityIssues: [] }; } } const result = validateInput(jsonString, 'json'); return { isValid: result.valid, errorMessage: !result.valid ? result.message : '', hasSecurity: result.securityIssues?.length > 0, securityIssues: result.securityIssues || [] }; } // Export all validation utilities export default { validateInput, validateRequestBody, createValidationMiddleware, validateUrl, validateEmail, validatePassword, validateJson };