Djangoの脆弱性CVE-2019-14232・CVE-2019-14233・CVE-2019-14234・CVE-2019-14235について解説

Ryuji Tsutsui
CreditEngine Tech
Published in
13 min readAug 26, 2019

こんにちは、筒井です。今回は2019年8月1日のリリースで修正されたDjangoの脆弱性CVE-2019-14232・CVE-2019-14233・CVE-2019-14234・CVE-2019-14235について解説します。
Django公式サイトでのリリース記事は以下を参照してください。

今回は脆弱性の数が多かったので、それらしいイメージ画像を選べませんでした😓

影響を受けるバージョン

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

  • Django master development branch
  • Django 2.2 before version 2.2.4
  • Django 2.1 before version 2.1.11
  • Django 1.11 before version 1.11.23

脆弱性の内容

【CVE-2019-4232: Denial-of-service possibility in django.utils.text.Truncator】

django.utils.text.Truncatorchars()word() メソッドに渡した文字列が正規表現の「バックトラッキング」を起こし、DoS(Denial-of-service)攻撃に繋がる場合があります。ただし、 html=True を指定した場合に限ります。
「バックトラッキング」での検索は一応意図通りの結果が得られますが、非常に効率が悪い処理になります。
「バックトラッキング」についてさらに詳しく知りたい人は、以下の記事を参照してください。

django.utils.text.Truncatorchars()word() メソッドは テンプレートフィルター truncatechars_htmltruncatedwords_html の内部で使われています。

【CVE-2019-14233: Denial-of-service possibility in strip_tags()】

django.utils.html.strip_tagsに渡した文字列が不完全なHTMLだった場合にDoS(Denial-of-service)攻撃に繋がる場合があります。
この関数はテンプレートフィルター striptags の内部で使われています。

【CVE-2019-14234: SQL injection possibility in key and index lookups for JSONField/HStoreField】

PostgreSQL用のモデルフィールドdjango.contrib.postgres.fields.JSONFielddjango.contrib.postgres.fields.HStoreField にSQLインジェクション脆弱性があります。

【CVE-2019-14235: Potential memory exhaustion in django.utils.encoding.uri_to_iri()】

django.utils.encoding.uri_to_iri にUTF-8として不正な値を渡すと、内部で再帰呼び出しを繰り返し、メモリが枯渇する可能性があります。

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

サンプルコードはDjango 2.2.3を使っています。

【CVE-2019-4232: Denial-of-service possibility in django.utils.text.Truncator】

サンプルコードでこの脆弱性を再現するのは難しいので、ソースコードに書かれている正規表現を抜き出して、Online regex tester and debuggerで検証してみましょう。
Django 2.2.3(脆弱性があるバージョン)の chars() で使われている正規表現は <.*?>|(.) です。(参照: 実際のコード
正規表現を<.*?>|(.)、対象の文字列を _X<<<<<<<<<<<> にした結果が以下のとおりです。画面右上に「3 matchs, 27 steps」 と表示されています。

対象の文字列の < を倍に増やしてみましょう。今度は「3 matchs, 38steps」になりました。

< の数をもっと増やすと、stepsの数がさらに増えていきます。

【CVE-2019-14233: Denial-of-service possibility in strip_tags()】

shell コマンドで以下のコードを実行すると結果が返ってこなくなります。
実際に動かしてみる場合は Ctrl + C で終了させてください。
スペックが低いマシンなら実行しないほうが無難です。

>>> value = '><!' + ('&' * 16000) + 'D'
>>> from django.utils.html import strip_tags
>>> strip_tags(value) # 結果が返ってこなくなる

【CVE-2019-14234: SQL injection possibility in key and index lookups for JSONField/HStoreField】

まず、ローカルにPostgreSQL 9.6系を用意し、 django223というデータベースを作成しておきます。
(デフォルトユーザーでパスワードなしで接続できるようにします)
psycopg2はバージョン2.8.3を使います。

次に、以下のコードを書きます。

shell コマンドで以下のデータを作ります。

>>> from example.models import Example
>>> Example.objects.create(value={'foo': 'bar'}, enabled=False)
<Example: Example object (1)>

上記データは条件にenabled=True を指定すれば見つけられないはずですが、以下のように valueフィールドに関する条件に OR 1 = 1 を含む文字列を渡すと、クエリにも OR 1 = 1 が挿入されてデータを見つけることができてしまいます。

>>> from example.models import Example
>>> Example.objects.filter(**{"""value__foo' = '"a"') OR 1 = 1 OR ('d""": 'x',}, enabled=True).exists() # 本来はFalseが返るはず
True

【CVE-2019-14235: Potential memory exhaustion in django.utils.encoding.uri_to_iri()】

この脆弱性は uri_to_iri の内部で呼ばれている関数 repercent_broken_unicodeのコードが原因です。repercent_broken_unicodeを使って検証してみましょう。

shell コマンドで以下のコードを実行してください。
なお、CVE-2019-14233と同様、スペックが低いマシンでは実行しないほうが無難です。

>>> import sys
>>> from django.utils.encoding import repercent_broken_unicode
>>> data = b'\xfc' * sys.getrecursionlimit()
>>> repercent_broken_unicode(data) # 実行するとshellからも抜ける
(中略)
RecursionError: maximum recursion depth exceeded

b'xfc'sys.getrecursionlimit() で取得した最大再帰数分繰り返しているところがポイントです。
本来は文字列が返ってくるはずですが、RecursionError が発生します。
shellからも抜けます)

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

変更されたコードの内容は以下のリリース記事「Resolution」を参照してください。

【CVE-2019-14232: Denial-of-service possibility in django.utils.text.Truncator】

以下メソッドで使われている正規表現が「バックトラッキング」を起こすのが原因でした。

  • word() メソッド→ <.*?>|((?:\w[-\w]*|&.*?;)+)
  • chars() メソッド→ <.*?>|(.)

上記の正規表現が以下に変更されました。

  • word() メソッド→ <[^>]+?>|([^<>\s]+)
  • chars() メソッド→ <[^>]+?>|(.)

先述の検証で使ったOnline regex tester and debuggerで、変更後の正規表現を試してみましょう。
(以下は chars()関数の正規表現を使った場合)

対象文字列の <をどんなに増やしても、stepsの数は変わりません。

【CVE-2019-14233: Denial-of-service possibility in strip_tags()】

strip_tags は以下の流れで変換処理を行います。
(参照: 実際のコード

  1. 変換前文字列(value)に <> が両方あるか→該当しないなら処理終了
  2. html.parser.HTMLParservalueを変換( new_value
  3. new_valueの長さ ≥ valueの長さ」か→該当するなら処理終了
  4. valuenew_value を入れて 1. に戻る

ところが、先述の攻撃例で使った文字列 '><!' + ('&' * 16000) + 'D' だとhtml.parser.HTMLParser を使っても <> を取り除くことができず、 1.3.の終了条件を満たさないため、延々とhtml.parser.HTMLParser を呼び続けることになります。

そこで、 3. の終了条件が以下に変更されました。

3. 「 valueに含まれる <の数 == new_valueに含まれる <の数」か→該当するなら処理終了

これにより、'><!' + ('&' * 16000) + 'D'3. の時点で処理終了となります。

【CVE-2019-14234: SQL injection possibility in key and index lookups for JSONField/HStoreField】

クエリを生成するロジックでは、以下のように2番目の %s に直に入力値を埋め込むようになっていたため、ここに任意のクエリを挿入することができました。

return "(%s -> '%s')" % (lhs, self.key_name), params

そこで、以下のように2番目の %s はそのまま %s を出力して、値の挿入はデータベース側に任せるように変更されました。

return '(%s -> %%s)' % lhs, [self.key_name] + params

【CVE-2019-14235: Potential memory exhaustion in django.utils.encoding.uri_to_iri()】

repercent_broken_unicode 関数(uri_to_iri の内部で呼ばれている関数)では、入力値にUTF-8として不正な値が含まれていると、原因となる文字を取り除いてから再帰呼び出しを行います。
ところが、先述の例で使った値 b'\xfc' * sys.getrecursionlimit() では、UTF-8として不正な文字 b’\xfc’が最大再帰数分含まれるため、再帰呼び出しできる回数の上限に達して、結果を返す前にRecursionError になってしまいます。
そこで、再帰呼び出しを止めて、代替する以下のコードに置き換えるようになりました。

path = path[:e.start] + force_bytes(repercent) + path[e.end:]

--

--

Ryuji Tsutsui
CreditEngine Tech

私がmediumに記事を書くことはおそらく当分ないので、 https://ryu22e.org/ の方を読んでください。