Opened 2 days ago
Last modified 2 days ago
#36152 new Cleanup/optimization
Annotation with `%` in alias fails at db level on Postgres and MySQL
Reported by: | Jacob Walls | Owned by: | |
---|---|---|---|
Component: | Database layer (models, ORM) | Version: | 5.1 |
Severity: | Normal | Keywords: | |
Cc: | Triage Stage: | Accepted | |
Has patch: | no | Needs documentation: | no |
Needs tests: | no | Patch needs improvement: | no |
Easy pickings: | no | UI/UX: | no |
Description (last modified by )
When developing with postgres, if you are creating dynamic annotations, you have to know to reject %
in the provided alias or transform it to %%
, otherwise you end up with an unhelpful exception from the db adapter.
test case:
-
tests/annotations/tests.py
diff --git a/tests/annotations/tests.py b/tests/annotations/tests.py index 29660a827e..6bc9bc9ed4 100644
a b class NonAggregateAnnotationTestCase(TestCase): 1171 1171 with self.assertRaisesMessage(ValueError, msg): 1172 1172 Book.objects.annotate(**{crafted_alias: Value(1)}) 1173 1173 1174 def test_annotation_contains_percent(self): 1175 Book.objects.annotate(**{"contains_percent%": F("pk")}).first() 1176 1174 1177 @skipUnless(connection.vendor == "postgresql", "PostgreSQL tests") 1175 1178 @skipUnlessDBFeature("supports_json_field") 1176 1179 def test_set_returning_functions(self):
Gives on psycopg2:
IndexError: tuple index out of range
On psycopg 3, the errors are more helpful, either:
django.db.utils.ProgrammingError: only '%s', '%b', '%t' are allowed as placeholders, got '%"'
Or if the annotation indeed included %s:
django.db.utils.ProgrammingError: the query has 2 placeholders but 1 parameters were passed
But I still think we could easily fail at the ORM level instead of going to the backend and failing there just quote %
to %%
.
The Oracle backend does this for you.
Change History (5)
comment:1 by , 2 days ago
Description: | modified (diff) |
---|
comment:3 by , 2 days ago
Summary: | Postgres backend could quote `%` in column aliases instead of failing at the db level → Annotation with `%` in alias fails at db level on postgres |
---|
I'd be in favor of adjusting the regex. That's essentially what my code does, and I could remove some workarounds if we get it in core.
I wouldn't be surprised if the fact we allow % today could be exploited one way to leak some parameter that should be present in the WHERE clause for example.
I did try to fiddle with that before deciding whether to go to the security team first. This had been on my medium term tinker list for a while and finally opened an issue today because I saw a similar issue for oracle in ticket:36147 waiting for a reproducer. (I was surprised to see the annotation "just work" on Oracle -- this is how I noticed that backend replaces %
for you.)
Do we need to worry about a deprecation path for non-postgres backends?
comment:4 by , 2 days ago
Summary: | Annotation with `%` in alias fails at db level on postgres → Annotation with `%` in alias fails at db level on Postgres and MySQL |
---|
Do we need to worry about a deprecation path for non-postgres backends?
I assume we would need a path yes, it's also crashing on MySQL though so not only a problem on Postgres
Traceback (most recent call last): File "/usr/local/lib/python3.12/site-packages/MySQLdb/cursors.py", line 200, in _mogrify query = query % args ~~~~~~^~~~~~ TypeError: not enough arguments for format string During handling of the above exception, another exception occurred: Traceback (most recent call last): File "/django/source/django/db/backends/utils.py", line 105, in _execute return self.cursor.execute(sql, params) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/django/source/django/db/backends/mysql/base.py", line 76, in execute return self.cursor.execute(query, args) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/usr/local/lib/python3.12/site-packages/MySQLdb/cursors.py", line 176, in execute mogrified_query = self._mogrify(query, args) ^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/usr/local/lib/python3.12/site-packages/MySQLdb/cursors.py", line 202, in _mogrify raise ProgrammingError(str(m)) MySQLdb.ProgrammingError: not enough arguments for format string
The specialized logic for Oracle originated from 2249bd275cbae6b73716bf36f5f36def1bb4222e which was likely to support introspecting legacy tables columns containing %
signs. Tat means we can't realistically deprecate the quote_name
path entirely. I haven't looked into why things work on SQLite.
comment:5 by , 2 days ago
Triage Stage: | Unreviewed → Accepted |
---|
I'm inclined to go with the "make FORBIDDEN_ALIAS_PATTERN
more restrictive" option, accepting the ticket on that basis.
If we are going to do that we'd need to be particularly careful here as annotations have been a vector of SQL injection in the past.
I think we should instead include
%
inFORBIDDEN_ALIAS_PATTERN
as by the time comes to escaping the SQL is mixed up with actual placeholders%s
and now alias names. I wouldn't be surprised if the fact we allow%
today could be exploited one way to leak some parameter that should be present in theWHERE
clause for example.The naive approach of adjusting
SQLCompiler.get_select
to escape percent signs could cause more harm than good as PEP-249 allows forCursor.execute
to be called with and withoutparams: tuple | None
and whenNone
is provided no parameter interpolation takes place so the prior escaping would now assign the wrong alias.In summary I think we should either wont-fix that one or accept it on the basis of augmenting
FORBIDDEN_ALIAS_PATTERN
as the complexity and security tradeoff are not worth the UX benefits it would provide (particularly because it has been broken for years already).