|
6 | 6 |
|
7 | 7 | from pyathena.error import ProgrammingError |
8 | 8 |
|
| 9 | +# A value whose single quote must not be allowed to terminate the string |
| 10 | +# literal. Trino and Athena do not treat a backslash as an escape character |
| 11 | +# inside a string literal, so a statement parsed by Trino must double the quote |
| 12 | +# ('') rather than backslash-escape it (\'). |
| 13 | +HOSTILE = "a' OR 1=1 --" |
| 14 | + |
| 15 | +# Statements parsed by the Trino/Athena engine, where single quotes must be |
| 16 | +# doubled. DELETE and CTAS (CREATE TABLE ... AS SELECT) are the paths that |
| 17 | +# CVE-2026-65321 left routed through the Hive escaper. |
| 18 | +TRINO_STATEMENTS = [ |
| 19 | + "SELECT * FROM t WHERE c = %(v)s", |
| 20 | + "WITH x AS (SELECT 1) SELECT * FROM t WHERE c = %(v)s", |
| 21 | + "INSERT INTO t VALUES (%(v)s)", |
| 22 | + "UPDATE t SET c = %(v)s", |
| 23 | + "MERGE INTO t USING s ON t.c = %(v)s", |
| 24 | + "DELETE FROM t WHERE c = %(v)s", |
| 25 | + "delete from t where c = %(v)s", |
| 26 | + "CREATE TABLE t WITH (format = 'PARQUET') AS SELECT * FROM s WHERE c = %(v)s", |
| 27 | + "CREATE VIEW v AS SELECT * FROM s WHERE c = %(v)s", |
| 28 | + "EXPLAIN DELETE FROM t WHERE c = %(v)s", |
| 29 | + "UNLOAD (SELECT * FROM t WHERE c = %(v)s) TO 's3://b/p/' WITH (format = 'PARQUET')", |
| 30 | + "/* generated by etl */ DELETE FROM t WHERE c = %(v)s", |
| 31 | + "-- nightly compaction\nDELETE FROM t WHERE c = %(v)s", |
| 32 | + "/* a */\n-- b\nDELETE FROM t WHERE c = %(v)s", |
| 33 | + "/* multi\nline */\nDELETE FROM t WHERE c = %(v)s", |
| 34 | + "\n\t DELETE FROM t WHERE c = %(v)s", |
| 35 | + # Near-miss statements: their prefixes are adjacent to the Hive allowlist |
| 36 | + # (ALTER/DROP/CREATE ... VIEW, VALUES) but Trino executes them, so they must |
| 37 | + # NOT be misrouted to the Hive escaper. |
| 38 | + "ALTER VIEW v AS SELECT name FROM s WHERE c = %(v)s", |
| 39 | + "DROP VIEW v -- %(v)s", |
| 40 | + "CREATE OR REPLACE VIEW v AS SELECT %(v)s", |
| 41 | + "VALUES (%(v)s)", |
| 42 | +] |
| 43 | + |
| 44 | +# Statements parsed by the Hive DDL engine, where backslash escaping is correct. |
| 45 | +HIVE_STATEMENTS = [ |
| 46 | + "CREATE EXTERNAL TABLE t (c string) LOCATION %(v)s", |
| 47 | + "CREATE TABLE t (c string) LOCATION %(v)s", |
| 48 | + "CREATE DATABASE d LOCATION %(v)s", |
| 49 | + "ALTER TABLE t SET LOCATION %(v)s", |
| 50 | + "DROP TABLE t PURGE -- %(v)s", |
| 51 | + "MSCK REPAIR TABLE t -- %(v)s", |
| 52 | + "SHOW PARTITIONS t -- %(v)s", |
| 53 | + "DESCRIBE t -- %(v)s", |
| 54 | + "CREATE SCHEMA s LOCATION %(v)s", |
| 55 | + "DROP DATABASE d -- %(v)s", |
| 56 | + "ALTER DATABASE d SET DBPROPERTIES ('k' = %(v)s)", |
| 57 | + # Leading comments must be stripped before detection on the Hive side too, |
| 58 | + # otherwise a commented Hive DDL statement would silently lose its escaping. |
| 59 | + "-- cleanup\nDROP TABLE t LOCATION %(v)s", |
| 60 | + "/* migrate */ ALTER TABLE t SET LOCATION %(v)s", |
| 61 | + "-- a\n-- b\nCREATE EXTERNAL TABLE t (c string) LOCATION %(v)s", |
| 62 | +] |
| 63 | + |
9 | 64 |
|
10 | 65 | class TestDefaultParameterFormatter: |
11 | 66 | def test_add_partition(self, formatter): |
@@ -468,3 +523,33 @@ def test_format_bad_parameter(self, formatter): |
468 | 523 | ["a string"], |
469 | 524 | ), |
470 | 525 | ) |
| 526 | + |
| 527 | + @pytest.mark.parametrize("operation", TRINO_STATEMENTS) |
| 528 | + def test_trino_statements_use_quote_doubling(self, formatter, operation): |
| 529 | + result = formatter.format(operation, {"v": HOSTILE}) |
| 530 | + assert "\\'" not in result, f"backslash escaping leaked into: {result}" |
| 531 | + assert "''" in result |
| 532 | + |
| 533 | + @pytest.mark.parametrize("operation", HIVE_STATEMENTS) |
| 534 | + def test_hive_ddl_keeps_backslash_escaping(self, formatter, operation): |
| 535 | + assert "\\'" in formatter.format(operation, {"v": HOSTILE}) |
| 536 | + |
| 537 | + def test_quote_cannot_terminate_literal_in_delete(self, formatter): |
| 538 | + result = formatter.format("DELETE FROM t WHERE c = %(v)s", {"v": HOSTILE}) |
| 539 | + # Every quote in the rendered value is doubled, so the literal opened by |
| 540 | + # the formatter is still open until its own closing quote. |
| 541 | + assert result.count("'") % 2 == 0 |
| 542 | + assert result == "DELETE FROM t WHERE c = 'a'' OR 1=1 --'" |
| 543 | + |
| 544 | + def test_ctas_detection_is_case_and_whitespace_insensitive(self, formatter): |
| 545 | + for operation in ( |
| 546 | + "create table t as select %(v)s", |
| 547 | + "CREATE\nTABLE\nt\nAS\nSELECT %(v)s", |
| 548 | + ): |
| 549 | + assert "\\'" not in formatter.format(operation, {"v": HOSTILE}) |
| 550 | + |
| 551 | + def test_table_name_starting_with_as_is_still_hive_ddl(self, formatter): |
| 552 | + # `as_of` must not be mistaken for the CTAS `AS` keyword. |
| 553 | + assert "\\'" in formatter.format( |
| 554 | + "CREATE EXTERNAL TABLE as_of (c string) LOCATION %(v)s", {"v": HOSTILE} |
| 555 | + ) |
0 commit comments