136 lines
3.7 KiB
TypeScript
136 lines
3.7 KiB
TypeScript
/**
|
|
* SQL Sanitizer - The Last Line of Defense
|
|
* Validates raw SQL before execution to prevent catastrophic accidents
|
|
*/
|
|
|
|
export interface SanitizationResult {
|
|
safe: boolean;
|
|
warnings: string[];
|
|
blocked?: string;
|
|
}
|
|
|
|
// Dangerous patterns that should NEVER be allowed
|
|
const BLOCKED_PATTERNS = [
|
|
/DROP\s+DATABASE/i,
|
|
/DROP\s+SCHEMA/i,
|
|
/ALTER\s+USER/i,
|
|
/ALTER\s+ROLE/i,
|
|
/CREATE\s+USER/i,
|
|
/CREATE\s+ROLE/i,
|
|
/GRANT\s+.*\s+TO/i,
|
|
/REVOKE\s+.*\s+FROM/i,
|
|
];
|
|
|
|
// Patterns that require special attention
|
|
const WARNING_PATTERNS = [
|
|
{ pattern: /TRUNCATE/i, message: 'TRUNCATE detected - use with caution' },
|
|
{ pattern: /DROP\s+TABLE/i, message: 'DROP TABLE detected - irreversible' },
|
|
{ pattern: /DELETE\s+FROM\s+\w+\s*;/i, message: 'DELETE without WHERE clause - will delete ALL rows' },
|
|
{ pattern: /UPDATE\s+\w+\s+SET\s+.*\s*;/i, message: 'UPDATE without WHERE clause - will update ALL rows' },
|
|
];
|
|
|
|
/**
|
|
* Validate table name is safe (alphanumeric + underscores only)
|
|
*/
|
|
export function validateTableName(tableName: string): boolean {
|
|
return /^[a-zA-Z0-9_]+$/.test(tableName);
|
|
}
|
|
|
|
/**
|
|
* Sanitize raw SQL before execution
|
|
*/
|
|
export function sanitizeSQL(sql: string): SanitizationResult {
|
|
const trimmed = sql.trim();
|
|
const warnings: string[] = [];
|
|
|
|
// Check for blocked patterns
|
|
for (const pattern of BLOCKED_PATTERNS) {
|
|
if (pattern.test(trimmed)) {
|
|
return {
|
|
safe: false,
|
|
warnings: [],
|
|
blocked: `Blocked dangerous command: ${pattern.source}`
|
|
};
|
|
}
|
|
}
|
|
|
|
// Check for warning patterns
|
|
for (const { pattern, message } of WARNING_PATTERNS) {
|
|
if (pattern.test(trimmed)) {
|
|
warnings.push(message);
|
|
}
|
|
}
|
|
|
|
// Validate table names in common operations
|
|
const tableMatches = trimmed.match(/(?:FROM|INTO|UPDATE|TRUNCATE|DROP TABLE)\s+([a-zA-Z0-9_]+)/gi);
|
|
if (tableMatches) {
|
|
for (const match of tableMatches) {
|
|
const tableName = match.split(/\s+/).pop() || '';
|
|
if (!validateTableName(tableName)) {
|
|
return {
|
|
safe: false,
|
|
warnings: [],
|
|
blocked: `Invalid table name: ${tableName}`
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
safe: true,
|
|
warnings
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Extract table names from SQL query
|
|
*/
|
|
export function extractTableNames(sql: string): string[] {
|
|
const matches = sql.match(/(?:FROM|INTO|UPDATE|TRUNCATE|DROP TABLE)\s+([a-zA-Z0-9_]+)/gi);
|
|
if (!matches) return [];
|
|
|
|
return matches.map(m => m.split(/\s+/).pop() || '').filter(Boolean);
|
|
}
|
|
|
|
/**
|
|
* Check if SQL contains WHERE clause
|
|
*/
|
|
export function hasWhereClause(sql: string): boolean {
|
|
return /WHERE\s+/i.test(sql);
|
|
}
|
|
|
|
/**
|
|
* More permissive check for maintenance window
|
|
* Used when run_mechanic flag is set
|
|
*/
|
|
export function sanitizeSQLForMaintenance(sql: string): SanitizationResult {
|
|
const trimmed = sql.trim();
|
|
const warnings: string[] = [];
|
|
|
|
// Still block the most dangerous commands
|
|
const criticalPatterns = [
|
|
/DROP\s+DATABASE/i,
|
|
/DROP\s+SCHEMA/i,
|
|
/ALTER\s+USER/i,
|
|
/CREATE\s+USER/i,
|
|
];
|
|
|
|
for (const pattern of criticalPatterns) {
|
|
if (pattern.test(trimmed)) {
|
|
return {
|
|
safe: false,
|
|
warnings: [],
|
|
blocked: `Blocked critical command even in maintenance mode: ${pattern.source}`
|
|
};
|
|
}
|
|
}
|
|
|
|
// Allow TRUNCATE and DROP TABLE in maintenance mode
|
|
warnings.push('Maintenance mode: dangerous operations allowed');
|
|
|
|
return {
|
|
safe: true,
|
|
warnings
|
|
};
|
|
}
|