2019年12月2日に修正されたDjangoの脆弱性CVE-2019-19118について解説します。
公式サイトでのリリース情報は以下を参照してください。
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のバージョンは以下の通りです。
まず、以下のモデルを用意します。
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ユーザーを作成します。パーミッションは以下のように設定します。
作成したら、上記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アップデート後に「親モデルは閲覧のみ許可・子モデルは編集を許可」権限の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) # これを追記
|