Class: Woods::Console::SqlValidator
- Inherits:
-
Object
- Object
- Woods::Console::SqlValidator
- Defined in:
- lib/woods/console/sql_validator.rb
Overview
Validates SQL strings for read-only safety.
Allows only SELECT and WITH…SELECT statements. Rejects DML (INSERT, UPDATE, DELETE), DDL (CREATE, DROP, ALTER, TRUNCATE), and administrative commands (GRANT, REVOKE). Also rejects multiple statements (semicolons).
Uses pattern-based validation, not full SQL parsing.
Constant Summary collapse
- FORBIDDEN_KEYWORDS =
Forbidden statement prefixes (case-insensitive).
Expanded beyond DML/DDL to cover:
-
PG procedural (‘DO`, `CALL`) which can run arbitrary plpgsql.
-
Session-state mutation (‘SET`, `RESET`) — `SET ROLE`, `SET search_path` can swap out the effective permission set for the rest of the session even under rollback.
-
Admin/cluster ops (‘VACUUM`, `ANALYZE`, `CLUSTER`, `REINDEX`, `REFRESH`, `LOCK`) which are reads in the English-language sense but carry side effects or heavy locks.
-
Async signalling (‘LISTEN`, `NOTIFY`).
-
Prepared-statement lifecycle (‘PREPARE`, `EXECUTE`, `DEALLOCATE`).
-
Transaction control (‘BEGIN`, `COMMIT`, `ROLLBACK`, `SAVEPOINT`, `RELEASE`, `START`) — SafeContext already owns the surrounding transaction; inner tx control would corrupt it.
-
File I/O vectors (‘LOAD`, `HANDLER`, `COPY`).
-
%w[ INSERT UPDATE DELETE DROP ALTER TRUNCATE CREATE GRANT REVOKE DO CALL SET RESET LISTEN NOTIFY VACUUM ANALYZE CLUSTER REINDEX REFRESH LOCK PREPARE EXECUTE DEALLOCATE BEGIN COMMIT ROLLBACK SAVEPOINT RELEASE START LOAD HANDLER COPY ].freeze
- BODY_FORBIDDEN_KEYWORDS =
Keywords that are forbidden anywhere in the SQL (not just at start).
UNION / INTERSECT / EXCEPT are SQL set operators — any of them can graft a second SELECT onto a validated one, which defeats the “single SELECT” posture even though TableGate still catches references to blocked tables. INTO / COPY are PostgreSQL write vectors that must not appear in read contexts.
%w[UNION INTERSECT EXCEPT INTO COPY].freeze
- DANGEROUS_FUNCTIONS =
Dangerous functions that can be used for DoS or file access.
%w[ pg_sleep lo_import lo_export pg_read_file pg_write_file load_file sleep benchmark ].freeze
- ALLOWED_PREFIXES =
Allowed statement prefixes (case-insensitive).
‘EXPLAIN ANALYZE` actually executes the planned query on PostgreSQL (and the MySQL 8.0+ `EXPLAIN ANALYZE` does the same) — explicitly reject the `ANALYZE` variant. PostgreSQL also accepts an option-list form `EXPLAIN (ANALYZE, FORMAT JSON) SELECT …` where `ANALYZE` follows `(` rather than whitespace; the `(?!s*(?s*ANALYZE)` lookahead rejects both spellings so SafeContext doesn’t silently trust “we’re just planning, not running” for what is a side-effectful execution. ‘EXPLAIN (…)` without `ANALYZE` is still permitted (e.g. `EXPLAIN (FORMAT JSON) SELECT 1`).
/\A\s*(SELECT|WITH|EXPLAIN(?!\s+ANALYZE)(?!\s*\([^)]*\bANALYZE\b))\b/i- FORBIDDEN_PREFIX_REGEXES =
Frozen map of forbidden keyword => regex matching the keyword at statement start. Used by #check_forbidden_keywords! and #check_forbidden_keywords_in_body!.
FORBIDDEN_KEYWORDS.to_h do |kw| [kw, /\A\s*#{kw}\b/i] end.freeze
- BODY_FORBIDDEN_REGEXES =
Frozen map of forbidden body keyword => regex matching the keyword anywhere. Used by #check_body_forbidden_keywords!.
BODY_FORBIDDEN_KEYWORDS.to_h do |kw| [kw, /\b#{kw}\b/i] end.freeze
- FORBIDDEN_BODY_REGEXES =
Frozen map of forbidden keyword => regex matching the keyword anywhere in the body. Used by #check_forbidden_keywords_in_body! for the whole-body scan.
FORBIDDEN_KEYWORDS.to_h do |kw| [kw, /\b#{kw}\b/i] end.freeze
- DANGEROUS_FUNCTION_REGEXES =
Frozen map of dangerous function name => regex matching a call to that function. Used by #check_dangerous_functions!.
DANGEROUS_FUNCTIONS.to_h do |func| [func, /\b#{func}\s*\(/i] end.freeze
Instance Method Summary collapse
-
#valid?(sql) ⇒ Boolean
Check if SQL is valid without raising.
- #validate!(sql) ⇒ Object
Instance Method Details
#valid?(sql) ⇒ Boolean
Check if SQL is valid without raising.
140 141 142 143 144 145 |
# File 'lib/woods/console/sql_validator.rb', line 140 def valid?(sql) validate!(sql) true rescue SqlValidationError false end |
#validate!(sql) ⇒ Object
105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 |
# File 'lib/woods/console/sql_validator.rb', line 105 def validate!(sql) raise SqlValidationError, 'SQL is empty' if sql.nil? || sql.strip.empty? normalized = sql.strip # Reject multiple statements (semicolons not inside string literals) if contains_multiple_statements?(normalized) raise SqlValidationError, 'Rejected: multiple statements are not allowed' end # Check for forbidden keywords at statement start check_forbidden_keywords!(normalized) # Check for writable CTEs (before body keywords to give better error messages) check_writable_ctes!(normalized) # Check for forbidden keywords anywhere in the SQL body check_body_forbidden_keywords!(normalized) # Check for dangerous functions check_dangerous_functions!(normalized) # After stripping comments, check again for forbidden keywords that might have been hidden check_forbidden_keywords_in_body!(normalized) # Must start with an allowed prefix return if normalized.match?(ALLOWED_PREFIXES) raise SqlValidationError, 'Rejected: SQL must start with SELECT, WITH, or EXPLAIN' end |