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

今回の脆弱性は権限昇格(Privilege escalation)に関するものです

今回の脆弱性は権限昇格(Privilege escalation)に関するものです

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

Django security releases issued: 2.2.8 and 2.1.15 | Weblog | Django

影響を受けるバージョン

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

  • Django master branch
  • Django 3.0
  • Django 2.2
  • Django 2.1

脆弱性の内容(Privilege escalation in the Django admin)

以下の条件すべてを満たす場合にadminで権限昇格(Privilege escalation)が可能になります。

  • 親子関係のモデルをadminで扱っている
  • 親モデルのadmin詳細画面で子モデルをインライン編集できるようにしている
  • 親モデルは閲覧のみ許可・子モデルは編集を許可されているadminユーザーがある

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

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

  • Django 2.2.7
  • Python 3.7.5

まず、以下のモデルを用意します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
"""books/models.py"""
import logging

from django.db import models

logger = logging.getLogger('django')


class Author(models.Model):
    name = models.CharField(verbose_name="名前", max_length=100)

    def __str__(self):
        return self.name

    def save(self, *args, **kwargs):
        logger.info("Author.save is called")
        super().save(*args, **kwargs)

    class Meta:
        verbose_name = "著者"
        verbose_name_plural = "著者"


class Book(models.Model):
    title = models.CharField(verbose_name="タイトル", max_length=100)
    author = models.ForeignKey(verbose_name="著者", to=Author, on_delete=models.CASCADE)

    def __str__(self):
        return self.title

    class Meta:
        verbose_name = "書籍"
        verbose_name_plural = "書籍"

admin.pyは以下のように書きます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
"""books/admin.py"""
from django.contrib import admin

from .models import Author, Book


class BookInline(admin.TabularInline):
    model = Book


class AuthorAdmin(admin.ModelAdmin):
    inlines = [
        BookInline,
    ]


admin.site.register(Author, AuthorAdmin)

シグナルも登録しておきます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
"""books/signals.py"""
import logging

from django.db.models.signals import pre_save, post_save
from django.dispatch import receiver

from .models import Author

logger = logging.getLogger('django')


@receiver(pre_save, sender=Author)
def pre_save_author(sender, **kwargs):
    logger.info("pre_save_author is called")


@receiver(post_save, sender=Author)
def post_save_author(sender, **kwargs):
    logger.info("post_save_author is called")
1
2
3
4
5
6
7
8
9
"""books/apps.py"""
from django.apps import AppConfig


class BooksConfig(AppConfig):
    name = 'books'

    def ready(self):
        from .signals import pre_save_author, post_save_author
1
2
"""books/__init__.py"""
default_app_config = 'books.apps.BooksConfig'

shellコマンド経由でデータを作っておきます。

1
2
3
4
5
6
7
8
>>> from books.models import Author, Book
>>> author = Author.objects.create(name='山田太郎')
>>> Book.objects.create(title='テスト1', author=author)
<Book: テスト1>
>>> Book.objects.create(title='テスト2', author=author)
<Book: テスト2>
>>> Book.objects.create(title='テスト3', author=author)
<Book: テスト3>

createsuperuserコマンドでスーパーユーザーを作成してからrunserverコマンドでアプリケーションを起動し、adminにログインします。

http://127.0.0.1:8000/admin/auth/user/add/から新しいadminユーザーを作成します。パーミッションは以下のように設定します。

User permissionsの設定

User permissionsの設定

作成したら、上記adminユーザーでログインし直します。

http://127.0.0.1:8000/admin/books/author/に表示されている著者データをクリックしてみましょう。以下のような画面になっているはずです。

著者詳細画面

著者詳細画面

画面下のSAVEボタンをクリックしてください。

画面上では著者は閲覧のみで編集できなくなっているように見えますが、実は裏側ではAuthor.saveとシグナルが呼ばれています。

runserverを実行しているターミナルを見ると、以下のログが出力されているはずです。

Author.save is called
pre_save_author is called
post_save_author is called

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

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

django.contrib.admin.ModelAdmin._changeform_viewの中がPOST時に「閲覧または変更権限以外ならPermissionDenied例外を投げる」というロジックになっていました。ところが、今回のように「親モデルは閲覧のみ許可・子モデルは編集を許可」の場合、親モデルは変更権限はないものの閲覧権限は持っているため、POSTも許可する挙動になってしまいました。

そこで、POST時に「変更権限がない場合にPermissionDenied例外を投げる」ように変更されました。

この変更により、「親モデルは閲覧のみ許可・子モデルは編集を許可」の場合に親モデルの詳細画面でPOSTを送信すると必ずPermissionDeniedが発生するようになり、子モデルの編集もできなくなりました。

django.contrib.admin.ModelAdmin.get_inline_formsetsでは親モデルに編集権限がなければ子モデルの編集権限も与えないコードに変更され、画面上でinputやSAVEボタンが表示されないようになっています。

先述のサンプルコードをDjango 2.2.8にアップデートしてからhttp://127.0.0.1:8000/admin/books/author/に表示されている著者データを表示させてみてください。以下のように表示されているはずです。

著者詳細画面(Django 2.2.8の場合)

著者詳細画面(Django 2.2.8の場合)

このままだと、Djangoアップデート後に「親モデルは閲覧のみ許可・子モデルは編集を許可」権限のadminユーザーは子モデルの編集ができなくなります。

これを回避するには、アップデート前にadmin.pyを確認して、admin.site.register(子モデル)の記述がなければ追記する必要があります。

今回のサンプルコードなら以下のように編集します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
"""books/admin.py"""
from django.contrib import admin

from .models import Author, Book


class BookInline(admin.TabularInline):
    model = Book


class AuthorAdmin(admin.ModelAdmin):
    inlines = [
        BookInline,
    ]


admin.site.register(Author, AuthorAdmin)
admin.site.register(Book)  # これを追記