/** * @fileoverview Unified SQL Expression and Query Validation * * Central utility for validating user-provided SQL expressions and full queries * against injection attacks. Used by RunView, aggregates, smart filters, ad-hoc * query execution, and any other feature accepting SQL input. * * Located in MJGlobal (lowest-level package) so all packages can use it. * * @module @memberjunction/global/SQLExpressionValidator */ import { BaseSingleton } from './BaseSingleton.js'; /** * Dangerous SQL keywords that are never allowed in user-provided expressions */ export declare const DANGEROUS_SQL_KEYWORDS: readonly ["DROP", "CREATE", "ALTER", "TRUNCATE", "RENAME", "INSERT", "UPDATE", "DELETE", "MERGE", "REPLACE", "GRANT", "REVOKE", "DENY", "EXEC", "EXECUTE", "CALL", "PROCEDURE", "FUNCTION", "BEGIN", "COMMIT", "ROLLBACK", "SAVEPOINT", "USE", "DATABASE", "SCHEMA", "IF", "WHILE", "LOOP", "FOR", "GOTO", "UNION", "INTERSECT", "EXCEPT", "EXISTS", "ANY", "ALL", "SOME", "BULK", "OPENROWSET", "OPENDATASOURCE", "OPENQUERY", "XP_", "SP_", "DYNAMIC", "PREPARE", "DEALLOCATE", "WAITFOR", "DELAY", "SLEEP", "SHUTDOWN", "RECONFIGURE"]; /** * Keywords from DANGEROUS_SQL_KEYWORDS that are legitimate in full SELECT queries. * These are only unblocked when context is 'full_query'. */ export declare const FULL_QUERY_ALLOWED_KEYWORDS: readonly ["EXISTS", "ANY", "ALL", "SOME", "UNION", "INTERSECT", "EXCEPT", "IF", "FOR"]; /** * System catalog / metadata objects that must never be referenced from a user-supplied * expression or ad-hoc query, in ANY context (including `full_query`). These sit outside * MemberJunction's entity-permission model, so allowing them turns a validated SELECT into * a schema-enumeration and credential-exfiltration primitive * (e.g. `SELECT name, password_hash FROM sys.sql_logins`, * `SELECT * FROM INFORMATION_SCHEMA.COLUMNS`, `SELECT * FROM pg_catalog.pg_authid`). * String literals are stripped before this check runs, so a literal value like `'sys.x'` is safe. */ export declare const BLOCKED_SYSTEM_OBJECT_PATTERNS: RegExp[]; /** * Removes SQL string literals from a clause or expression so that a keyword denylist can be * applied to the *code* portion without tripping over keywords that appear inside quoted data * (e.g. `Comments LIKE '%--%'`). * * 🚨 SECURITY — this is the single implementation of literal-stripping for MJ's SQL screens, and * it MUST stay byte-for-byte consistent with how the database parses literals. If the stripper * removes a span the database does NOT treat as a literal, everything hidden inside that span * bypasses the denylist while the database still executes it. * * An earlier version of this logic (duplicated in two places, which is how it survived) honored * **backslash escaping** — `/(['"])(?:(?=(\\?))\2[\s\S])*?\1/g`. SQL Server and PostgreSQL do not * treat `\` as an escape character, so `x = 'a\') ; DROP TABLE Users; --'` was swallowed whole as * one "literal" and stripped to `x = `, which passed every denylist — while the database closed * the literal at the real quote and executed the stacked statement. * * **Do NOT reintroduce backslash-escape handling here, and do NOT inline a second copy of this * regex anywhere else — call this function.** * * @param sql The clause, expression, or query to strip literals from * @returns The input with every complete string literal removed. An UNTERMINATED literal is left * in place on purpose: the stray quote and everything after it stay visible to the * denylist rather than being silently swallowed. */ export declare function StripSQLStringLiterals(sql: string): string; /** * Safe SQL functions allowed in expressions, organized by category */ export declare const ALLOWED_SQL_FUNCTIONS: { readonly aggregates: readonly ["COUNT", "COUNT_BIG", "SUM", "AVG", "MIN", "MAX", "STDEV", "STDEVP", "VAR", "VARP", "STRING_AGG", "CHECKSUM_AGG"]; readonly math: readonly ["ABS", "CEILING", "FLOOR", "ROUND", "POWER", "SQRT", "LOG", "LOG10", "EXP", "SIGN", "RAND"]; readonly string: readonly ["LEN", "LENGTH", "UPPER", "LOWER", "LTRIM", "RTRIM", "TRIM", "LEFT", "RIGHT", "SUBSTRING", "CHARINDEX", "REPLACE", "CONCAT", "STUFF"]; readonly date: readonly ["DATEPART", "DATEDIFF", "DATEADD", "YEAR", "MONTH", "DAY", "HOUR", "MINUTE", "SECOND", "GETDATE", "GETUTCDATE", "SYSDATETIME", "EOMONTH"]; readonly conversion: readonly ["CAST", "CONVERT", "TRY_CAST", "TRY_CONVERT", "FORMAT"]; readonly nullHandling: readonly ["ISNULL", "COALESCE", "NULLIF", "IIF"]; readonly conditional: readonly ["CASE", "WHEN", "THEN", "ELSE", "END"]; readonly logical: readonly ["AND", "OR", "NOT", "IS", "NULL", "LIKE", "BETWEEN", "IN"]; readonly ordering: readonly ["ASC", "ASCENDING", "DESC", "DESCENDING", "OVER", "PARTITION", "BY", "ORDER", "ROWS", "RANGE", "UNBOUNDED", "PRECEDING", "FOLLOWING", "CURRENT", "ROW"]; }; /** * Validation context - affects what's allowed */ export type SQLValidationContext = 'where_clause' | 'order_by' | 'aggregate' | 'field_reference' | 'full_query'; /** * Validation result with detailed error information */ export interface SQLValidationResult { /** Whether the expression passed validation */ valid: boolean; /** Error message if validation failed */ error?: string; /** Specific keyword or pattern that triggered the error */ trigger?: string; /** Suggested fix if available */ suggestion?: string; } /** * Options for SQL expression validation */ export interface SQLValidationOptions { /** Validation context affects what's allowed */ context: SQLValidationContext; /** Entity field names for validation (optional - enables field checking) */ entityFields?: string[]; /** Whether to require at least one aggregate function (for 'aggregate' context). Default: true for aggregate context */ requireAggregate?: boolean; /** Whether to allow SELECT keyword (normally blocked for subquery prevention) */ allowSubqueries?: boolean; /** Custom allowed keywords/functions to add */ additionalAllowed?: string[]; /** Custom blocked keywords to add */ additionalBlocked?: string[]; } /** * Central SQL expression validator for preventing SQL injection. * * Provides context-aware validation for different types of SQL expressions * (WHERE clauses, ORDER BY, aggregates, etc.) with detailed error reporting. * * @example * ```typescript * const validator = SQLExpressionValidator.Instance; * * // Validate an aggregate expression * const result = validator.validate('SUM(OrderTotal)', { * context: 'aggregate', * entityFields: ['OrderTotal', 'Quantity', 'Price'] * }); * * if (!result.valid) { * console.error(result.error); * } * ``` */ export declare class SQLExpressionValidator extends BaseSingleton { /** * Use SQLExpressionValidator.Instance to get the singleton instance. */ constructor(); /** * Gets the singleton instance of the validator */ static get Instance(): SQLExpressionValidator; /** * Validate a SQL expression for injection and allowed patterns. * * @param expression The SQL expression to validate * @param options Validation options including context and entity fields * @returns Validation result with error details if invalid */ validate(expression: string, options: SQLValidationOptions): SQLValidationResult; /** * Remove string literals to avoid false positives in keyword detection. * * 🚨 SECURITY: delegates to {@link StripSQLStringLiterals} — read the warning there before * changing anything about how literals are matched. This must never grow its own regex again. */ private removeStringLiterals; /** * Check for dangerous SQL patterns that indicate injection attempts */ private checkDangerousPatterns; /** * Check that function names are in the allowlist */ private checkFunctionNames; /** * Context-specific validation rules */ private checkContextRules; /** * Validate field references exist in entity (lenient mode - just for logging) */ private checkFieldReferences; /** * Strip SQL comments (single-line -- and multi-line block comments) from a query. * Used by full_query context to allow agent-generated header comments * without triggering the comment injection check. */ private stripSQLComments; /** * Escape special regex characters in a string */ private escapeRegex; /** * Normalize literal escape sequences in SQL strings. * Agent-generated SQL sometimes arrives with literal \n, \r, \t sequences * (backslash + letter) instead of actual whitespace characters. This happens * when JSON is double-escaped or the SQL passes through a transport layer * that doesn't interpret escape sequences. Without normalization, comment * stripping fails because the regex expects real newlines. */ private normalizeSQLWhitespace; /** * Validate a full SQL query (SELECT or WITH/CTE statement). * Blocks mutations, dangerous operations, and multi-statement injection. * Allows SELECT, subqueries, set operations, and SQL comments. */ validateFullQuery(sql: string): SQLValidationResult; } //# sourceMappingURL=SQLExpressionValidator.d.ts.map