/** * tests/analytics/analyst-sql-bypass-corpus.ts * ============================================= * BI-P017 — the bypass attempts, in one place, so the SAME corpus can be run * twice: once against the guard (unit) and once against a LIVE ClickHouse with * the guard REMOVED (integration), where the grants and the view predicate * must refuse it anyway. * * Running it only against the guard would prove the guard, not the system. * This estate has repeatedly shipped guards that were never tested against a * real attempt: INF0's tenant-from-body hole was exploitable against LIVE * production, and ST0RE turned out to have FOUR cross-tenant paths, not the * one reported. * * The integration test asserts that with the guard removed the engine either * ERRORS or returns zero foreign rows — never that "the guard said no", which * is the assertion that would keep passing while the system was open. * * Session under test: tenant `t_alpha`, catalog `t_alpha_sql`, objects * `sales` and `events`. The targets are `t_beta_sql`, the shared base database * `opt_analytics`, and `system`. */ export interface BypassAttempt { id: string; /** What the attacker is trying to do. */ goal: string; sql: string; } export const BYPASS_CORPUS: BypassAttempt[] = [ // ── reach another tenant's catalog ────────────────────────────────────── { id: 'cross-catalog-direct', goal: "read the other tenant's catalog", sql: 'SELECT * FROM t_beta_sql.sales', }, { id: 'cross-catalog-union', goal: 'union in the other tenant', sql: 'SELECT customer FROM t_alpha_sql.sales UNION ALL SELECT customer FROM t_beta_sql.sales', }, { id: 'cross-catalog-subquery', goal: 'subquery on the other tenant', sql: 'SELECT (SELECT count() FROM t_beta_sql.sales) AS n FROM t_alpha_sql.sales LIMIT 1', }, { id: 'cross-catalog-join', goal: 'join the other tenant', sql: 'SELECT a.customer FROM t_alpha_sql.sales a JOIN t_beta_sql.sales b ON a.customer = b.customer', }, { id: 'cross-catalog-cte', goal: 'hide the other tenant in a CTE', sql: 'WITH other AS (SELECT * FROM t_beta_sql.sales) SELECT count() AS n FROM other', }, { id: 'cross-catalog-quoted', goal: 'quote the identifier to dodge the scan', sql: 'SELECT * FROM "t_beta_sql"."sales"', }, { id: 'cross-catalog-backtick', goal: 'backtick the identifier', sql: 'SELECT * FROM `t_beta_sql`.`sales`', }, // ── reach the unfiltered base table ───────────────────────────────────── { id: 'base-table-direct', goal: 'read the shared base table, which holds every tenant', sql: 'SELECT * FROM opt_analytics.opt_analytics_event', }, { id: 'base-table-in-cte', goal: 'base table behind a CTE', sql: 'WITH raw AS (SELECT * FROM opt_analytics.opt_analytics_event) SELECT count() AS n FROM raw', }, { id: 'base-table-union', goal: 'union the base table in', sql: 'SELECT customer FROM t_alpha_sql.sales UNION ALL SELECT tenant_id FROM opt_analytics.opt_analytics_event', }, // ── system / metadata catalogs ────────────────────────────────────────── { id: 'system-tables', goal: 'enumerate every table on the server', sql: 'SELECT database, name FROM system.tables', }, { id: 'system-columns', goal: 'enumerate columns', sql: 'SELECT * FROM system.columns' }, { id: 'system-users', goal: 'enumerate users', sql: 'SELECT * FROM system.users' }, { id: 'system-processes', goal: "read other tenants' running queries", sql: 'SELECT query FROM system.processes', }, { id: 'system-query-log', goal: "read other tenants' statement text out of the log", sql: 'SELECT query FROM system.query_log', }, { id: 'information-schema', goal: 'metadata via the ANSI schema', sql: 'SELECT * FROM information_schema.tables', }, { id: 'one-table', goal: 'the always-present one-row table', sql: 'SELECT * FROM system.one' }, // ── table functions: these need NO table privilege at all ─────────────── { id: 'tf-url', goal: 'exfiltrate over HTTP', sql: "SELECT * FROM url('http://127.0.0.1:1/x', 'JSONEachRow', 'a String')", }, { id: 'tf-file', goal: 'read the server filesystem', sql: "SELECT * FROM file('/etc/passwd', 'LineAsString', 'l String')", }, { id: 'tf-s3', goal: 'read object storage directly', sql: "SELECT * FROM s3('http://minio/b/*', 'CSV', 'a String')", }, { id: 'tf-remote', goal: 'query another ClickHouse server', sql: "SELECT * FROM remote('127.0.0.1:9000', 'opt_analytics', 'opt_analytics_event')", }, { id: 'tf-mysql', goal: 'reach an external database', sql: "SELECT * FROM mysql('h:3306', 'db', 't', 'u', 'p')", }, { id: 'tf-executable', goal: 'run a program', sql: "SELECT * FROM executable('sh', 'TSV', 'a String')" }, { id: 'tf-numbers', goal: 'a CPU bomb that touches no table', sql: 'SELECT count() FROM numbers_mt(100000000000)', }, { id: 'tf-generate', goal: 'unbounded synthetic scan', sql: "SELECT * FROM generateRandom('a String')" }, { id: 'tf-merge', goal: 'merge() glob across a database', sql: "SELECT * FROM merge('opt_analytics', '.*')", }, { id: 'tf-cluster', goal: 'fan out across the cluster', sql: "SELECT * FROM cluster('default', 'opt_analytics', 'opt_analytics_event')", }, { id: 'tf-view', goal: 'wrap a subquery in view()', sql: 'SELECT * FROM view(SELECT * FROM opt_analytics.opt_analytics_event)', }, { id: 'fn-dictget', goal: 'read a dictionary, bypassing table grants', sql: "SELECT dictGet('d', 'v', toUInt64(1)) AS x FROM t_alpha_sql.sales", }, // ── write / DDL / privilege escalation ────────────────────────────────── { id: 'stacked-drop', goal: 'stacked statement', sql: 'SELECT 1 FROM t_alpha_sql.sales; DROP TABLE opt_analytics.opt_analytics_event', }, { id: 'insert', goal: 'write', sql: "INSERT INTO t_alpha_sql.sales VALUES ('x')" }, { id: 'insert-select', goal: 'write from a read', sql: 'INSERT INTO t_alpha_sql.sales SELECT * FROM t_alpha_sql.sales', }, { id: 'alter-delete', goal: 'mutate', sql: 'ALTER TABLE opt_analytics.opt_analytics_event DELETE WHERE 1' }, { id: 'create-view-escape', goal: 'create an unfiltered view to read later', sql: 'CREATE VIEW t_alpha_sql.escape AS SELECT * FROM opt_analytics.opt_analytics_event', }, { id: 'grant-self', goal: 'grant itself more', sql: 'GRANT SELECT ON opt_analytics.* TO CURRENT_USER' }, { id: 'system-command', goal: 'operate the server', sql: 'SYSTEM RELOAD DICTIONARIES' }, { id: 'kill-query', goal: "kill another tenant's query", sql: 'KILL QUERY WHERE 1' }, { id: 'truncate', goal: 'destroy data', sql: 'TRUNCATE TABLE t_alpha_sql.sales' }, { id: 'optimize', goal: 'trigger heavy background work', sql: 'OPTIMIZE TABLE opt_analytics.opt_analytics_event FINAL', }, { id: 'outfile', goal: 'write to the filesystem', sql: "SELECT * FROM t_alpha_sql.sales INTO OUTFILE '/tmp/x.csv'", }, // ── lift the governance ceiling from inside the statement ─────────────── { id: 'settings-raise-rows', goal: 'raise max_rows_to_read', sql: 'SELECT count() AS n FROM t_alpha_sql.sales SETTINGS max_rows_to_read = 0', }, { id: 'settings-readonly-off', goal: 'turn readonly off', sql: 'SELECT 1 FROM t_alpha_sql.sales SETTINGS readonly = 0', }, { id: 'set-statement', goal: 'SET before the query', sql: 'SET max_execution_time = 0' }, { id: 'format-override', goal: 'choose the wire format', sql: 'SELECT * FROM t_alpha_sql.sales FORMAT Native', }, // ── lexical evasion of the keyword scan ───────────────────────────────── { id: 'comment-split-name', goal: 'hide a catalog boundary behind a comment', sql: 'SELECT * FROM opt_analytics/**/.opt_analytics_event', }, { id: 'comment-line-hide', goal: 'hide the tail after a -- comment', sql: 'SELECT customer FROM t_alpha_sql.sales -- \nUNION ALL SELECT name FROM system.tables', }, { id: 'comment-hash-hide', goal: 'hide the tail after a # comment', sql: 'SELECT customer FROM t_alpha_sql.sales # \nUNION ALL SELECT name FROM system.tables', }, { id: 'unterminated-comment', goal: 'run off the end of a comment', sql: 'SELECT * FROM t_alpha_sql.sales /* x', }, { id: 'unterminated-string', goal: 'run off the end of a literal', sql: "SELECT * FROM t_alpha_sql.sales WHERE customer = 'x", }, { id: 'unicode-homoglyph', // U+0430 CYRILLIC SMALL LETTER A in place of the ASCII "a". goal: 'cyrillic homoglyph inside the catalog name', sql: 'SELECT * FROM t_\u0430lpha_sql.sales', }, { id: 'zero-width', // U+200B ZERO WIDTH SPACE inside "system". goal: 'zero-width space inside a reserved catalog name', sql: 'SELECT * FROM sys\u200btem.tables', }, { id: 'nul-byte', goal: 'NUL truncation between two halves of a statement', sql: 'SELECT customer FROM t_alpha_sql.sales \u0000 UNION ALL SELECT name FROM system.tables', }, { id: 'case-mixing', goal: 'mixed case keywords and names', sql: 'SeLeCt * FrOm SyStEm.TaBlEs' }, { id: 'whitespace-newlines', goal: 'newlines and tabs between tokens', sql: 'SELECT\n\t*\nFROM\n\tsystem.tables', }, { id: 'describe-subquery', goal: 'DESCRIBE a base table from inside a subquery, where the FROM-scan sees only a paren', sql: 'SELECT * FROM (DESCRIBE TABLE opt_analytics.opt_analytics_event)', }, { id: 'desc-subquery-short', goal: 'the same, using the DESC abbreviation', sql: 'SELECT * FROM (DESC TABLE opt_analytics.opt_analytics_event)', }, { id: 'current-user', goal: "identify the session's ClickHouse principal", sql: 'SELECT currentUser() AS u FROM t_alpha_sql.sales', }, { id: 'three-part-name', goal: 'a three-part name', sql: 'SELECT * FROM a.b.c' }, { id: 'unqualified-table', goal: "unqualified name resolved by the connection's default database", sql: 'SELECT * FROM opt_analytics_event', }, { id: 'cte-shadow', goal: 'shadow a legitimate name with a CTE while also reading the real base table', sql: 'WITH sales AS (SELECT 1 AS x) SELECT count() AS n FROM opt_analytics.opt_analytics_event', }, { id: 'no-from', goal: 'a statement that reads no table at all', sql: 'SELECT 1' }, { id: 'object-not-granted', goal: 'an object in the right catalog that the session was not given', sql: 'SELECT * FROM t_alpha_sql.secret_ledger', }, ]; /** * Attempts the GUARD ADMITS — and which only layers 3 and 4 refuse. * * This list exists because the first run of the corpus found one, and hiding * it by quietly deleting the case would have been the dishonest fix. The guard * reasons about statement STRUCTURE; it does not know which columns a view * projects, and teaching it would mean giving it a schema cache that goes * stale — a hardcoded list, which is how api0's container job died this week. * * `SELECT tenant_id FROM t_alpha_sql.sales` is structurally a legitimate read * of an allowed object. It fails at the ENGINE, because the view does not * project `tenant_id` at all (tenant-catalog.ts refuses to build one that * does). The integration test asserts that engine-side refusal directly, so * this case is covered by evidence rather than by an assumption. */ export const GUARD_ADMITS_ENGINE_REFUSES: BypassAttempt[] = [ { id: 'hidden-tenant-column', goal: 'select the tenant column the view does not project', sql: 'SELECT tenant_id FROM t_alpha_sql.sales', }, { id: 'hidden-column-star-then-filter', goal: 'filter on a column the view does not expose', sql: "SELECT count() AS n FROM t_alpha_sql.sales WHERE tenant_id = 't_beta'", }, ]; /** Statements that MUST be admitted — a guard that refuses everything is useless. */ export const LEGITIMATE_CORPUS: Array<{ id: string; sql: string }> = [ { id: 'simple', sql: 'SELECT customer, amount FROM t_alpha_sql.sales LIMIT 10' }, { id: 'aggregate', sql: 'SELECT customer, sum(amount) AS total FROM t_alpha_sql.sales GROUP BY customer ORDER BY total DESC LIMIT 5', }, { id: 'cte', sql: 'WITH top AS (SELECT customer, sum(amount) AS total FROM t_alpha_sql.sales GROUP BY customer) ' + 'SELECT customer, total FROM top ORDER BY total DESC LIMIT 3', }, { id: 'self-join', sql: 'SELECT a.customer FROM t_alpha_sql.sales a JOIN t_alpha_sql.events b ON a.customer = b.customer LIMIT 10', }, { id: 'literal-with-quotes', sql: "SELECT count() AS n FROM t_alpha_sql.sales WHERE customer = 'O''Brien'", }, { id: 'literal-containing-sql-text', sql: "SELECT count() AS n FROM t_alpha_sql.sales WHERE customer = 'DROP TABLE system.tables'", }, { id: 'comment-then-query', sql: '-- daily revenue\nSELECT sum(amount) AS total FROM t_alpha_sql.sales' }, { id: 'subquery', sql: 'SELECT customer FROM t_alpha_sql.sales WHERE amount > (SELECT avg(amount) FROM t_alpha_sql.sales)', }, { id: 'union-own-catalog', sql: 'SELECT customer FROM t_alpha_sql.sales UNION ALL SELECT customer FROM t_alpha_sql.events', }, { id: 'trailing-semicolon', sql: 'SELECT count() AS n FROM t_alpha_sql.sales;' }, ];