Posted on :: Updated on ::

PyAthena 3.35.4 fixes a SQL injection in the default parameter formatter. The bug affected DELETE and CREATE TABLE ... AS SELECT (CTAS) statements built with PyAthena's parameter substitution. An application that passed an untrusted string as a parameter to one of those statements could let that string break out of its quotes and run as SQL. All released versions up to and including 3.35.3 are affected, and the fix is in 3.35.4.

How PyAthena quotes parameters

PyAthena implements the DB-API pyformat parameter style. When an application calls cursor.execute(sql, params), PyAthena substitutes each parameter into the SQL text after quoting it, rather than sending the value to Athena as a bound parameter. Quoting a string here means wrapping it in single quotes and escaping any single quote inside it, so that the value stays one string literal. The formatter is the code that does this quoting.

Two escapers, chosen by statement prefix

The formatter carried two different escapers. One doubles a single quote, turning ' into ''; this is the ANSI SQL rule that Trino and Athena follow. The other backslash-escapes it, turning ' into \'; this is the Hive and MySQL rule. Both conventions were present because Athena runs two kinds of statement, SQL through its Trino engine and a smaller set of DDL through a Hive-derived engine, and the two engines read string literals by different rules.

The formatter picked the escaper from the start of the statement. SELECT, WITH, INSERT, UPDATE, and MERGE went to the quote-doubling escaper. Everything else went to the backslash escaper.

Why DELETE and CTAS were injectable

DELETE was missing from that first list, and a CTAS statement starts with CREATE, so both fell through to the backslash escaper. That would be harmless if Athena read \' as an escaped quote. It does not. Trino and Athena treat a backslash inside a single-quoted literal as an ordinary character, so \' is a literal backslash followed by a quote that ends the string.

The effect is easiest to see in the rendered SQL. Given the parameter value a' OR 1=1 --, the formatter produced this for a DELETE:

DELETE FROM t WHERE c = 'a\' OR 1=1 --'

Athena reads the \ as a backslash and the following ' as the end of the string literal. The OR 1=1 then runs as part of the WHERE clause, and the -- comments out the trailing quote. A predicate meant to match one row now matches every row. The same break-out works in a CTAS SELECT, where an injected UNION SELECT can copy data the caller never meant to expose into the new table.

Why it went unnoticed

The escaper selection had no test that passed a quote-containing string through DELETE or CTAS. The tests that did cover those paths used values without quotes, so the wrong escaper produced output that looked correct. An earlier fix had already moved UPDATE and MERGE onto the safe escaper, so the mismatch was understood in the abstract, but DELETE and CTAS were left behind and no test caught them.

Defaulting to the safe escaper

3.35.4 inverts the choice. The formatter now uses the quote-doubling escaper by default, and routes a statement to the backslash escaper only when it recognizes that statement as Hive DDL: CREATE TABLE or CREATE EXTERNAL TABLE that is not a CTAS, ALTER, DROP of a database, schema, or table, MSCK REPAIR, SHOW, and DESCRIBE. Leading comments are stripped before this check, so a comment cannot hide the statement type.

The default matters because the two mistakes are not symmetric. Doubling a quote in a Hive DDL statement is at worst a formatting quirk, since the Hive lexer joins adjacent string literals. Backslash-escaping a quote in a Trino statement is a break-out. When the statement type is unclear, the formatter now falls back to the escaper that cannot be exploited.

Coordinated disclosure

The issue was reported through VulnCheck, acting as CNA, on behalf of the researcher Rahul Karne. The proof of concept runs PyAthena's real formatter and executes the generated SQL against a local DuckDB database, which reproduces the break-out without touching AWS. It is tracked as CVE-2026-65321 and GitHub advisory GHSA-xwj5-g6cv-4r5c, scored CVSS 3.1 9.8 (Critical), and CVSS 4.0 9.3 in the CVE record. That score is the worst case for a library of this kind, since it assumes untrusted input reaches the vulnerable parameter directly; the real exposure of a given application depends on whether it passes untrusted values into DELETE or CTAS parameters.

What to do

Upgrade to PyAthena 3.35.4:

pip install -U 'pyathena>=3.35.4'

Until the upgrade lands, avoid passing untrusted values as parameters to DELETE or CTAS statements. Building those statements from trusted input only, or routing the untrusted value through a SELECT- or INSERT-shaped query, avoids the vulnerable escaper.

What I took from it

Two habits would have caught this earlier. The first is to test the security-relevant branch with a hostile value rather than a benign one; a single quote in a DELETE parameter would have failed loudly. The second is to make the default the safe one, so that a gap in the statement-type detection fails closed instead of open. The longer-term direction is to move parameter values off client-side escaping and onto Athena's server-side parameters, which removes this class of bug at the source.

References

Table of Contents