
Django SQL injection vulnerability
Django is an open-source Web Application Framework written in Python, built according to the MVC (Model - View - Controller) model. It was originally built to manage news content websites owned by the Lawrence publishing corporation, as CMS (Content Management System) software.
Django versions 3.1.x through 3.1.13 and versions 3.2.x through 3.2.5 contain an SQL injection vulnerability.
The cause of this vulnerability is that the input data filtering function for user-controlled data in QuerySet.order_by() is insufficient to prevent SQL injection attacks. This vulnerability can be exploited to allow attackers to perform unauthorized actions, leading to the leak of sensitive data.
| CVE - ID | CVE-2021-35042 |
|---|---|
| Severity | 9.8 - CRITICAL |
| CWE - ID | CWE-89: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection') |
| Vulnerability Publication Date | 1/7/2021 |
| Affected Software | 3.1.x < 3.1.13, 3.2.x < 3.2.5 |
| Requires Authentication | Not required |
In Django, creating tables and defining the fields in the database is done by declaring a model class in the models.py file. In this example, we declare a table named Wolf and a field named name.


Django's built-in ORM framework is used to interact with the database, and the result of a query is a set, which is a QuerySet.
order_by(fields)
By default, order_by() returns a QuerySet sorted in an order specified in the ordering option in the Model's Meta. We can override the order_by condition in each query by using the order_by() method.
Example
wolves = Wolf.objects.order_by('-name', 'id')
The result of the above query will be sorted in descending order by the name field, then ascending by id. The minus sign before the field name name indicates that the results are sorted in descending order.
The following example sorts the returned results by the field received from the user; if no value is passed in, it will sort by the
idfield.
Result

In versions 3.1 and 3.2, Django allows combining query methods with table names in the order_by query. This is also the main cause of this vulnerability.
Passing a table name gives us the same result as passing a field name as usual
cve202135042_wolf is the table name
First, the application directly calls the order_by() function; the code that handles the order_by() function is defined at:
django/db/models/query.py

The order_by() function does 2 things
- Clears all current methods being called by order_by() and removes the default parameter passed in when order_by receives a different value.
- Passes the parameter into order_by. The
add_ordering()function performs this task
def add_ordering(self, *ordering):
"""
Add items from the 'ordering' sequence to the query's "order by"
clause. These items are either field names (not column names) --
possibly with a direction prefix ('-' or '?') -- or OrderBy
expressions.
If 'ordering' is empty, clear all ordering from the query.
"""
errors = []
for item in ordering:
if isinstance(item, str):
if '.' in item:
warnings.warn(
'Passing column raw column aliases to order_by() is '
'deprecated. Wrap %r in a RawSQL expression before '
'passing it to order_by().' % item,
category=RemovedInDjango40Warning,
stacklevel=3,
)
continue
if item == '?':
continue
if item.startswith('-'):
item = item[1:]
if item in self.annotations:
continue
if self.extra and item in self.extra:
continue
# names_to_path() validates the lookup. A descriptive
# FieldError will be raise if it's not.
self.names_to_path(item.split(LOOKUP_SEP), self.model._meta)
elif not hasattr(item, 'resolve_expression'):
errors.append(item)
if getattr(item, 'contains_aggregate', False):
raise FieldError(
'Using an aggregate in order_by() without also including '
'it in annotate() is not allowed: %s' % item
)
if errors:
raise FieldError('Invalid order_by arguments: %s' % errors)
if ordering:
self.order_by += ordering
else:
self.default_ordering = False
The parameter passed to add_ordering() is an array.
For example, when the parameter is passed in as follows:
wolves = Wolf.objects.order_by( 'name' , 'id' )Then, the application will convert it into the following database query:
SELECT "cve202135042_wolf"."id", "cve202135042_wolf"."name" FROM "cve202135042_wolf" ORDER BY "cve202135042_wolf"."name" ASC, "cve202135042_wolf"."id" ASC
When passed in, the add_ordering function will check each element in the array; if it is a string, it will be checked against the following 5 cases:
if '.' in item:It checks whether it is a query with a column name and whether that column has a table name specified in the SQL statement. If so, it issues a warning andcontinues.if item == '?':If the element value is the '?' sign, the output results will be sorted randomly, thencontinue.if item.startswith('-'):If the item starts with the '-' character, the query results will be sorted DESC (Descending).if item in self.annotations:It checks whether it contains a comment; if so,continue.if self.extra and item in self.extra:Determines whether there are extras; if so,continue.
After the 5 checks, the parameter is then passed to the function self.names_to_path(item.split(LOOKUP_SEP), self.model._meta) to continue checking whether it is a valid column name; then, if valid, it is added to self.ordering of the Query class for further processing.
Django's ORM filters the data inserted into queries very strictly, but this source code change leading to SQL injection is because the author hypothesized that if the column name is a UUID (Universal Unique Identifier) column, the order_by query could not be executed.
That is, if the input data is xxx-xxx-xxx-xxx (UUID format), the query cannot be executed.
Code before the change
# django/db/models/sql/constants.py
ORDER_PATTERN = _lazy_re_compile ( r '\?|[-+]?[.\w]+$' )
# django/db/models/sql/query.py
def add_ordering ( self , * ordering ):
errors = []
for item in ordering :
if isinstance ( item , str ) and ORDER_PATTERN . match ( item ):
if '.' in item :
warnings . warn (
'Passing column raw column aliases to order_by() is '
'deprecated. Wrap %r in a RawSQL expression before '
'passing it to order_by().' % item ,
category = RemovedInDjango40Warning ,
stacklevel = 3 ,
)
elif not hasattr ( item , 'resolve_expression' ):
errors . append ( item )
if getattr ( item , 'contains_aggregate' , False ):
raise FieldError (
'Using an aggregate in order_by() without also including '
'it in annotate() is not allowed: %s ' % item
)
if errors :
raise FieldError ( 'Invalid order_by arguments: %s ' % errors )
if ordering :
self . order_by += ordering
else :
self . default_ordering = False
From the code above, we can see that if the parameter matches ? or starts with - followed by regular characters or a . sign, then the query is executed.
Therefore, when a column name is a UUID, it is an invalid value and cannot be passed to order_by.
The change to this handling code was accepted, and it was changed as follows: https://github.com/charettes/django/commit/513948735b799239f3ef8c89397592445e1a0cd5

It used the self.name_to_path function to validate the input data.
But after checking, if . is present in the item, it treats it as a query with a table name; the continue statement is executed, which directly skips using the self.name_to_path function to validate the data.
The code handling the . in the get_order_by function is as follows:
django/db/models/sql/compiler.py
if '.' in field :
table , col = col . split ( '.' , 1 )
order_by . append ((
OrderBy (
RawSQL ( ' %s . %s ' % (
self . quote_name_unless_alias ( table ), col ), [ ]),
descending = descending
), False ))
continue
The self.quote_name_unless_alias function handles the table name, filters valid table names, and skips filtering the column name, so we can inject an SQL injection statement.
In the current Django 4.0 version, querying by table name using . has been removed and is no longer supported; patches were released for versions 3.1 and 3.2. Versions 3.2 through 3.2.4 and 3.1 through 3.1.12 are affected.
3.2.x Fixed CVE-2021-35042 -- Prevented SQL injection in QuerySet.o…
3.1.x Fixed CVE-2021-35042 -- Prevented SQL injection in QuerySet.o…
The fix is very simple: the old ReGex data validation was brought back.

Update Django to a non-affected version.
Docker & Docker-compose
git clone https://github.com/LUUANHDUC/CVE-2021-35042.git./setup.sh for initial setupsudo docker-compose up --buildsudo docker exec -it cve-2021-35042_web_1 python manage.py makemigrations cve202135042sudo docker exec -it cve-2021-35042_web_1 python manage.py migratehttp://localhost:8000/wolves/?order_by=nameThe screen after the installation is complete

Condition: To be able to exploit it, we must know the table name somehow :))
When injecting the statement, we must know the table name in order to execute the SQLi statement.
When entering an incorrect table name

When entering the correct table name, the order_by query executes normally.

The statement at this point will become
SELECT "cve202135042_wolf"."id", "cve202135042_wolf"."name" FROM "cve202135042_wolf" ORDER BY ("cve202135042_wolf"."name") ASC
At this point, we can terminate the preceding order_by statement and inject an SQL statement to exploit it.

SELECT "cve202135042_wolf"."id", "cve202135042_wolf"."name" FROM "cve202135042_wolf" ORDER BY ("cve202135042_wolf"."name"); SELECT * from cve202135042_wolf where id =1; --) ASC
https://www.djangoproject.com/weblog/2021/jul/01/security-releases/ https://xz.aliyun.com/t/9834 https://www.bugxss.com/vulnerability-report/3095.html https://blankheart.top/2022/04/07/cve-2021-35042/ https://itcn.blog/p/1648921763575859.html https://github.com/YouGina/CVE-2021-35042