Djangoの脆弱性CVE-2019-14232・CVE-2019-14233・CVE-2019-14234・CVE-2019-14235について解説
こんにちは、筒井です。今回は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.Truncator
の chars()
・ word()
メソッドに渡した文字列が正規表現の「バックトラッキング」を起こし、DoS(Denial-of-service)攻撃に繋がる場合があります。ただし、 html=True
を指定した場合に限ります。
「バックトラッキング」での検索は一応意図通りの結果が得られますが、非常に効率が悪い処理になります。
「バックトラッキング」についてさらに詳しく知りたい人は、以下の記事を参照してください。
django.utils.text.Truncator
の chars()
・ word()
メソッドは テンプレートフィルター truncatechars_html
・ truncatedwords_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.JSONField
・django.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
は以下の流れで変換処理を行います。
(参照: 実際のコード)
- 変換前文字列(
value
)に<
と>
が両方あるか→該当しないなら処理終了 html.parser.HTMLParser
でvalue
を変換(new_value
)- 「
new_value
の長さ ≥value
の長さ」か→該当するなら処理終了 value
にnew_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:]