From 1119eb6461d1fa7ab31c8466378de1b46bd18a73 Mon Sep 17 00:00:00 2001 From: Saddam Date: Wed, 24 Jun 2026 11:06:25 +0530 Subject: [PATCH] fix(django-google-spanner): escape tzname in datetime sql helpers --- .../django_spanner/operations.py | 17 ++++++ .../unit/django_spanner/test_operations.py | 56 +++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/packages/django-google-spanner/django_spanner/operations.py b/packages/django-google-spanner/django_spanner/operations.py index 4fa71a1b41b0..02bd148da9aa 100644 --- a/packages/django-google-spanner/django_spanner/operations.py +++ b/packages/django-google-spanner/django_spanner/operations.py @@ -22,6 +22,18 @@ ) +def _escape_tzname(tzname): + """Escape a time zone name for embedding in a Spanner string literal. + + The datetime helpers below inline the time zone name into a quoted + string literal. Cloud Spanner (GoogleSQL) uses backslash escaping inside + string literals, so a name containing a quote would otherwise close the + literal and inject SQL. Both quote characters are escaped so the result is + safe in single- and double-quoted contexts. + """ + return tzname.replace("\\", "\\\\").replace('"', '\\"').replace("'", "\\'") + + class DatabaseOperations(BaseDatabaseOperations): """A Spanner-specific version of Django database operations.""" @@ -431,6 +443,7 @@ def datetime_extract_sql(self, lookup_type, field_name, params, tzname): :returns: A SQL statement for extracting. """ tzname = tzname if settings.USE_TZ and tzname else "UTC" + tzname = _escape_tzname(tzname) lookup_type = self.extract_names.get(lookup_type, lookup_type) return ( 'EXTRACT(%s FROM %s AT TIME ZONE "%s")' @@ -518,6 +531,7 @@ def datetime_trunc_sql(self, lookup_type, field_name, params, tzname="UTC"): """ # https://cloud.google.com/spanner/docs/functions-and-operators#timestamp_trunc tzname = tzname if settings.USE_TZ and tzname else "UTC" + tzname = _escape_tzname(tzname) if lookup_type == "week": # Spanner truncates to Sunday but Django expects Monday. First, # subtract a day so that a Sunday will be truncated to the previous @@ -553,6 +567,7 @@ def time_trunc_sql(self, lookup_type, field_name, params, tzname="UTC"): """ # https://cloud.google.com/spanner/docs/functions-and-operators#timestamp_trunc tzname = tzname if settings.USE_TZ and tzname else "UTC" + tzname = _escape_tzname(tzname) return ( 'TIMESTAMP_TRUNC(%s, %s, "%s")' % ( @@ -581,6 +596,7 @@ def datetime_cast_date_sql(self, field_name, params, tzname): """ # https://cloud.google.com/spanner/docs/functions-and-operators#date tzname = tzname if settings.USE_TZ and tzname else "UTC" + tzname = _escape_tzname(tzname) return 'DATE(%s, "%s")' % (field_name, tzname), params def datetime_cast_time_sql(self, field_name, params, tzname): @@ -600,6 +616,7 @@ def datetime_cast_time_sql(self, field_name, params, tzname): :returns: A SQL statement for casting. """ tzname = tzname if settings.USE_TZ and tzname else "UTC" + tzname = _escape_tzname(tzname) # Cloud Spanner doesn't have a function for converting # TIMESTAMP to another time zone. return ( diff --git a/packages/django-google-spanner/tests/unit/django_spanner/test_operations.py b/packages/django-google-spanner/tests/unit/django_spanner/test_operations.py index bbadf5129e22..bced89228c1a 100644 --- a/packages/django-google-spanner/tests/unit/django_spanner/test_operations.py +++ b/packages/django-google-spanner/tests/unit/django_spanner/test_operations.py @@ -189,6 +189,62 @@ def test_datetime_cast_time_sql_use_tz_false(self): ) settings.USE_TZ = True # reset changes. + def test_datetime_extract_sql_escapes_tzname(self): + settings.USE_TZ = True + self.assertEqual( + self.db_operations.datetime_extract_sql( + "year", "dummy_field", None, 'X" OR "a"="a' + ), + ( + 'EXTRACT(year FROM dummy_field AT TIME ZONE "X\\" OR \\"a\\"=\\"a")', + None, + ), + ) + + def test_datetime_trunc_sql_escapes_tzname(self): + settings.USE_TZ = True + self.assertEqual( + self.db_operations.datetime_trunc_sql( + "day", "dummy_field", None, 'X" OR "a"="a' + ), + ( + 'TIMESTAMP_TRUNC(dummy_field, day, "X\\" OR \\"a\\"=\\"a")', + None, + ), + ) + + def test_time_trunc_sql_escapes_tzname(self): + settings.USE_TZ = True + self.assertEqual( + self.db_operations.time_trunc_sql( + "day", "dummy_field", None, 'X" OR "a"="a' + ), + ( + 'TIMESTAMP_TRUNC(dummy_field, day, "X\\" OR \\"a\\"=\\"a")', + None, + ), + ) + + def test_datetime_cast_date_sql_escapes_tzname(self): + settings.USE_TZ = True + self.assertEqual( + self.db_operations.datetime_cast_date_sql( + "dummy_field", None, 'X" OR "a"="a' + ), + ('DATE(dummy_field, "X\\" OR \\"a\\"=\\"a")', None), + ) + + def test_datetime_cast_time_sql_escapes_tzname(self): + settings.USE_TZ = True + self.assertEqual( + self.db_operations.datetime_cast_time_sql("dummy_field", None, "X' || 'a"), + ( + "TIMESTAMP(FORMAT_TIMESTAMP('%Y-%m-%d %R:%E9S %Z', " + "dummy_field, 'X\\' || \\'a'))", + None, + ), + ) + def test_date_interval_sql(self): self.assertEqual( self.db_operations.date_interval_sql(timedelta(days=1)),