ctrlshiftleft
Version:
AI-powered toolkit for embedding QA and security testing into development workflows
293 lines (257 loc) • 8.74 kB
text/typescript
/**
* 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
};