English edition

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

CVE-2020-13254はデータ漏洩の可能性があります

CVE-2020-13254はデータ漏洩の可能性があります

CVE-2020-13596はXSSに関する脆弱性です

CVE-2020-13596はXSSに関する脆弱性です

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

Django security releases issued: 3.0.7 and 2.2.13 | Weblog | Django

影響を受けるバージョン

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

  • Django master branch
  • Django 3.1
  • Django 3.0
  • Django 2.2

脆弱性の内容

CVE-2020-13254: Potential data leakage via malformed memcached keys

memcachedを使っている場合に、不正なキーを渡すことでデータの漏洩を起こす可能性があります。

CVE-2020-13596: Possible XSS via admin ForeignKeyRawIdWidget

ForeignKeyRawIdWidgetがadmin上で生成するクエリパラメータにXSS脆弱性があります。

この脆弱性は、モデルにForeignKey.limit_choices_toManyToManyField.limit_choices_toを指定したフィールドがある場合に発生する可能性があります。

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

サンプルコードで使っている環境のバージョンは以下の通りです。

  • Django 3.0.6
  • pylibmc 1.6.1
  • Python 3.8.3
  • memcached 1.6.6
  • libmemcached 1.0.18

CVE-2020-13254: Potential data leakage via malformed memcached keys

まず、公式ドキュメントに従ってsettings.pyを編集します。以下が設定例です。

1
2
3
4
5
6
CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.memcached.PyLibMCCache',
        'LOCATION': '127.0.0.1:11211',  # memcachedを置いている場所に応じて適宜変更すること
    }
}

次に、shellコマンド上で以下を実行します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
>>> from django.core.cache import cache
>>> cache.set('my_key', 'hello, world!')
>>> cache.get('my_key')
'hello, world!'
>>> c = chr(33)
>>> c
'!'
>>> cache.get('my_key' + c)
>>> c = chr(32)
>>> c
' '
>>> cache.get('my_key' + c)  # Noneが返ってくるはず
'hello, world!'

最後のcache.get('my_key' + c)Noneが返ってくるはずですが、'my_key'キーで設定された値'hello, world!'を取得できてしまいました。

CVE-2020-13596: Possible XSS via admin ForeignKeyRawIdWidget

以下のコードを用意します。

example/models.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
from django.db import models


class Band(models.Model):
    name = models.CharField(max_length=100)

    def __str__(self):
        return self.name


class UnsafeLimitChoicesTo(models.Model):
    band = models.ForeignKey(
        Band,
        models.CASCADE,
        limit_choices_to={'name': '"&><escapeme'},
    )

example/admin.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
from django.contrib import admin

from . import models


admin.site.register(models.Band)


class UnsafeLimitChoicesToAdmin(admin.ModelAdmin):
    raw_id_fields = (
        'band',
    )


admin.site.register(models.UnsafeLimitChoicesTo, UnsafeLimitChoicesToAdmin)

makemigrationsコマンド・migrateコマンドでデータベースを作成し、createsuperuserコマンドでadminユーザーを作成してください。

shellコマンドで以下のデータも作っておきます。

1
2
3
>>> from example.models import Band
>>> Band.objects.create(name='"&><escapeme')
<Band: "&><escapeme>

runserverコマンドで開発サーバーを立ち上げ、以下URLからUnsafeLimitChoicesToを作成します。Band1を指定してください。

http://127.0.0.1:8000/admin/example/unsafelimitchoicesto/add/

登録後のデータを表示して、以下の虫眼鏡ボタン(赤枠部分)のHTMLを確認してみましょう。

http://127.0.0.1:8000/admin/example/unsafelimitchoicesto/1/change/ の表示内容

http://127.0.0.1:8000/admin/example/unsafelimitchoicesto/1/change/ の表示内容

以下のように"&><escapemeの部分がエスケープされずに埋め込まれています。

1
<a href="/admin/example/band/?name="&><escapeme&amp;_to_field=id" class="related-lookup" id="lookup_id_band" title="Lookup"></a>&nbsp;<strong><a href="/admin/example/band/1/change/">&quot;&amp;&gt;&lt;escapeme</a></strong>

limit_choices_to'"></a><script>alert(\'XSS\');</script><a href="'のような値にすれば、adminにスクリプトを仕込むことも可能です。

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

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

CVE-2020-13254:

CVE-2020-13596:

CVE-2020-13254: Potential data leakage via malformed memcached keys

django.core.cache.backends.memcached.BaseMemcachedCacheではmemcachedにキーを渡す際に入力値をそのまま渡していました。

そこで、validate_keyメソッドを追加して入力値を検査し、不正な値が含まれている場合はmemcachedにキーを渡さず、警告メッセージを出力させるようになりました。

Django 3.0.7で先述の「脆弱性を利用した攻撃の例」に載せたコードを実行すると、以下のメッセージが表示されます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
>>> from django.core.cache import cache
>>> cache.set('my_key', 'hello, world!')
>>> cache.get('my_key')
'hello, world!'
>>> c = chr(33)
>>> c
'!'
>>> cache.get('my_key' + c)
>>> c = chr(32)
>>> c
' '
>>> cache.get('my_key' + c)
****/.venv/lib/python3.7/site-packages/django/core/cache/backends/base.py:253: CacheKeyWarning: Cache key contains characters that will cause errors if used with memcached: ':1:my_key '
  'used with memcached: %r' % key, CacheKeyWarning

CVE-2020-13596: Possible XSS via admin ForeignKeyRawIdWidget

django.contrib.admin.widgets.ForeignKeyRawIdWidget.get_contextの中で、related_url変数(limit_choices_toの値を虫眼鏡ボタンに埋め込むために使う)にdjango.utils.safestring.mark_safeを使った値を渡していたことが原因でした。django.utils.safestring.mark_safeを使った値はテンプレート上でエスケープされません。

修正後のコードではdjango.utils.safestring.mark_safeを使わずdjango.utils.http.urlencodeを使うようになりました。

Django 3.0.7で先述の「脆弱性を利用した攻撃の例」に載せたコードを実行すると、"&><escapemeがエスケープされるようになります。

1
<a href="/admin/example/band/?name=%22%26%3E%3Cescapeme&amp;_to_field=id" class="related-lookup" id="lookup_id_band" title="Lookup"></a>&nbsp;<strong><a href="/admin/example/band/1/change/">&quot;&amp;&gt;&lt;escapeme</a></strong>