Opened 4 weeks ago
Last modified 21 hours ago
#36152 assigned Cleanup/optimization
Annotation with `%` in alias fails at db level on Postgres and MySQL
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:
-
TabularUnified 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.
According to the ticket's flags, the next step(s) to move this issue forward are:
- For anyone except the patch author to review the patch using the patch review checklist and either mark the ticket as "Ready for checkin" if everything looks good, or leave comments for improvement and mark the ticket as "Patch needs improvement".
Change History (8)
comment:1 by , 4 weeks ago
Description: | modified (diff) |
---|
comment:3 by , 4 weeks 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 , 4 weeks 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 , 4 weeks 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.
comment:8 by , 21 hours ago
I see merit in handling the two issues separately.
#6343 refers to table and column names, which are rarely if ever driven by user input and potentially outside the developer's control for legacy databases (see the end of comment:4). This is a harder problem.
With this ticket's focus on aliases, which could be controlled by a user (or a user of a reusable app, e.g. supplying dynamic annotations), I'm seeing a consensus for deprecating the buggy behavior and a reasonable upgrade path, since there are no database entities to adjust.
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
.