English edition

2020年2月3日に修正されたDjangoの脆弱性CVE-2020-7471について解説します。

今回の脆弱性はSQLインジェクションが可能になるものです

今回の脆弱性はSQLインジェクションが可能になるものです

公式サイトでのリリース情報は以下を参照してください。

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

影響を受けるバージョン

以下のバージョンが影響を受けます。

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

脆弱性の内容[Potential SQL injection via StringAgg(delimiter)]

PostgreSQLのSTRING_AGG関数を利用するためのクラスdjango.contrib.postgres.aggregates.StringAggにSQLインジェクション脆弱性がありました。初期化時にdelimiterパラメーターに渡す値に任意のクエリを埋め込むことが可能です。

脆弱性を利用した攻撃の例

サンプルコードで使っているライブラリ・Python・データベースのバージョンは以下の通りです。

  • ライブラリ
    • Django 3.0.2
    • psycopg2-binary 2.8.4
  • Python 3.8.1
  • データベース
    • PostgreSQL 9.6.16

予め、PostgreSQL上にデータベースを作成し、DjangoプロジェクトのDATABASESをPostgreSQLに接続する設定に変更しておいてください。(以下は変更例)

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

Djangoプロジェクトにexampleというアプリケーションを追加し、以下のコードを書きます。

example/models.py

1
2
3
4
5
from django.db import models


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

./manage.py makemigrations && ./manage.py migrateを実行で今回使うデータベーステーブルが作られます。 shellコマンド上でテストデータも作っておきましょう。

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)>

続いて、shellコマンド上でStringAggを使ってみましょう。まずは正しい挙動について確認します。

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'}

delimiterに指定した;を挟んだ文字列foo;bar;testが作成されました。 ところが、delimiter="'"にした場合にはこうなります。

 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>
(中略)
    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...

実際に生成されるクエリの全体像も見てみましょう。

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"'

delimiterに渡した文字がエスケープされていないため、構文エラーのSQLが生成されています。工夫すれば、delimiterに別のクエリを挿入することもできそうです。

どのコードに問題があったか・どのように修正されたか

実際の変更内容は以下のGitHubコードを参照してください。

django.contrib.postgres.aggregates.StringAgg.__init__の中ではdelimiterをPythonの文字列埋め込みで式に直接挿入するコードになっていました。 そこで、delimiterを一旦django.db.models.Valueでラップして、値の埋め込みをデータベース側に任せるコードに変更されました。 自前でdelimiterをエスケープしているわけではないので、この変更に漏れがあって別のパターンのSQLインジェクションが発生することはありません。