From 302b7d8e7ba7188b66fe0d4d9398db6d64ef27e9 Mon Sep 17 00:00:00 2001 From: Sakthivel Subramanian Date: Tue, 21 Jul 2026 17:50:03 +0000 Subject: [PATCH] fix(spanner): escape embedded backticks in dbapi escape_name In google-cloud-spanner DB-API, escape_name() did not check for or double embedded backtick characters (`) when wrapping identifiers. This allowed identifiers containing backticks to break out of backtick-quoted identifier scopes (CWE-89 identifier injection). This commit updates escape_name() to: - Detect embedded backtick characters in identifier names. - Escape internal backticks by doubling them (replace("`", "``")) when enclosing identifiers in backticks. - Add unit test cases for embedded backticks in test_escape_name. --- .../google/cloud/spanner_dbapi/parse_utils.py | 29 ++++++++++++++++--- .../unit/spanner_dbapi/test_parse_utils.py | 18 ++++++++++++ 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/packages/google-cloud-spanner/google/cloud/spanner_dbapi/parse_utils.py b/packages/google-cloud-spanner/google/cloud/spanner_dbapi/parse_utils.py index 90907c6a779e..c00aaf245ee2 100644 --- a/packages/google-cloud-spanner/google/cloud/spanner_dbapi/parse_utils.py +++ b/packages/google-cloud-spanner/google/cloud/spanner_dbapi/parse_utils.py @@ -382,10 +382,13 @@ def ensure_where_clause(sql): return sql + " WHERE 1=1" +_QUOTED_IDENTIFIER_RE = re.compile(r"\A`(\\.|[^`\\])*`\Z", re.DOTALL) + + def escape_name(name): """ - Apply backticks to the name that either contain '-' or - ' ', or is a Cloud Spanner's reserved keyword. + Apply backticks to the name if it is not a valid regular ASCII identifier, + or if it is a Cloud Spanner's reserved keyword. :type name: str :param name: Name to escape. @@ -393,6 +396,24 @@ def escape_name(name): :rtype: str :returns: Name escaped if it has to be escaped. """ - if "-" in name or " " in name or name.upper() in SPANNER_RESERVED_KEYWORDS: - return "`" + name + "`" + if not name: + return name + + if _QUOTED_IDENTIFIER_RE.match(name): + return name + + if "." in name: + parts = name.split(".") + return ".".join(escape_name(part) for part in parts) + + is_valid_regular_identifier = ( + (name[0].isalpha() or name[0] == "_") + and all(c.isalnum() or c == "_" for c in name) + and name.isascii() + ) + + if not is_valid_regular_identifier or name.upper() in SPANNER_RESERVED_KEYWORDS: + escaped = name.replace("\\", "\\\\").replace("`", "\\`") + return f"`{escaped}`" + return name diff --git a/packages/google-cloud-spanner/tests/unit/spanner_dbapi/test_parse_utils.py b/packages/google-cloud-spanner/tests/unit/spanner_dbapi/test_parse_utils.py index 64000a0ae12d..29a08edb4264 100644 --- a/packages/google-cloud-spanner/tests/unit/spanner_dbapi/test_parse_utils.py +++ b/packages/google-cloud-spanner/tests/unit/spanner_dbapi/test_parse_utils.py @@ -403,6 +403,24 @@ def test_escape_name(self): ("with space", "`with space`"), ("name", "name"), ("", ""), + ("col`; DROP TABLE t; -- x", "`col\\`; DROP TABLE t; -- x`"), + ("table`name", "`table\\`name`"), + ("`", "`\\``"), + ("col/*comment*/name", "`col/*comment*/name`"), + ("123column", "`123column`"), + ("col;select", "`col;select`"), + ("col\nname", "`col\nname`"), + ("test\\", "`test\\\\`"), + ("my_schema.my_table", "my_schema.my_table"), + ("my-schema.my-table", "`my-schema`.`my-table`"), + ("`my_table`", "`my_table`"), + ("`my_schema`.`my_table`", "`my_schema`.`my_table`"), + ("`table.with.dots`", "`table.with.dots`"), + ("`col\\`; DROP TABLE users; --`", "`col\\`; DROP TABLE users; --`"), + ( + "`col\\\\`; DROP TABLE users; --`", + "`\\`col\\\\\\\\\\`; DROP TABLE users; --\\``", + ), ) for name, want in cases: with self.subTest(name=name):