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

Reported by: Jacob Walls Owned by: Jacob Walls
Component: Database layer (models, ORM) Version: 5.1
Severity: Normal Keywords:
Cc: Mariusz Felisiak Triage Stage: Accepted
Has patch: yes Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description (last modified by Jacob Walls)

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):  
    11711171                with self.assertRaisesMessage(ValueError, msg):
    11721172                    Book.objects.annotate(**{crafted_alias: Value(1)})
    11731173
     1174    def test_annotation_contains_percent(self):
     1175        Book.objects.annotate(**{"contains_percent%": F("pk")}).first()
     1176
    11741177    @skipUnless(connection.vendor == "postgresql", "PostgreSQL tests")
    11751178    @skipUnlessDBFeature("supports_json_field")
    11761179    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 Jacob Walls, 4 weeks ago

Description: modified (diff)

comment:2 by Simon Charette, 4 weeks ago

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 % in FORBIDDEN_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 the WHERE 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 for Cursor.execute to be called with and without params: tuple | None and when None 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.

Version 1, edited 4 weeks ago by Simon Charette (previous) (next) (diff)

comment:3 by Jacob Walls, 4 weeks ago

Summary: Postgres backend could quote `%` in column aliases instead of failing at the db levelAnnotation 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 Simon Charette, 4 weeks ago

Summary: Annotation with `%` in alias fails at db level on postgresAnnotation 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 Natalia Bidart, 4 weeks ago

Triage Stage: UnreviewedAccepted

I'm inclined to go with the "make FORBIDDEN_ALIAS_PATTERN more restrictive" option, accepting the ticket on that basis.

comment:6 by Jacob Walls, 8 days ago

Has patch: set
Owner: set to Jacob Walls
Status: newassigned

comment:7 by Mariusz Felisiak, 36 hours ago

Cc: Mariusz Felisiak added

Is not this a duplicate of #6343?

comment:8 by Jacob Walls, 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.

Note: See TracTickets for help on using tickets.
Back to Top