Opened 10 months ago

Closed 5 months ago

Last modified 2 months ago

#36152 closed Cleanup/optimization (fixed)

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: Ready for checkin
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:

  • 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.

Change History (12)

comment:1 by Jacob Walls, 10 months ago

Description: modified (diff)

comment:2 by Simon Charette, 10 months 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 as the complexity and security tradeoff are not worth the UX benefits it would provide (particularly because it has been broken for years already).

Last edited 10 months ago by Simon Charette (previous) (diff)

comment:3 by Jacob Walls, 10 months 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, 10 months 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, 10 months 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, 10 months ago

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

comment:7 by Mariusz Felisiak, 9 months ago

Cc: Mariusz Felisiak added

Is not this a duplicate of #6343?

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

comment:9 by Sarah Boyce, 5 months ago

Triage Stage: AcceptedReady for checkin

comment:10 by Sarah Boyce <42296566+sarahboyce@…>, 5 months ago

Resolution: fixed
Status: assignedclosed

In 8ede411:

Fixed #36152 -- Deprecated use of "%" in column aliases.

Unintentional support existed only on SQLite and Oracle.

comment:11 by Jacob Walls <jacobtylerwalls@…>, 3 months ago

In 2d453a2a:

Refs #36152 -- Suppressed duplicate warning when using "%" in alias via values().

comment:12 by Jacob Walls <jacobtylerwalls@…>, 2 months ago

In fd705912:

Refs #36152, #35667 -- Used skip_file_prefixes in alias deprecation warning.

Follow-up to 8ede411a81b40ca53362e6788601193c7e56a0cf.

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