はじめに
上達の早さは人それぞれだから、nikkieです。
Webアプリケーションの脆弱性の1つ、XSS(クロスサイト・スクリプティング)。
悪意を持ったHTMLやJavaScriptコードを注入できてしまう脆弱性です。
今回はDjangoにおけるXSS対策を理解するために、はじめの一歩を踏み出した記録です。
目次
HttpResponseによるレスポンスとXSS
Djangoのビュー関数から返すと、その内容をWebブラウザに表示できるHttpResponse
1。
ビュー関数でreturn HttpResponse("Hello world")
と返すと、ブラウザには「Hello world」と表示されます。
HttpResponse
を扱う際、XSSに注意です。
たとえばブラウザに「<script>alert("Hello world")</script>
」と表示したいときに、return HttpResponse('<script>alert("Hello world")</script>')
としてはいけません
これは『実践Django』4.4でも取り上げられているまずい例です。
return HttpResponse('<script>alert("Hello world")</script>')
としてしまうと、scriptタグがブラウザで実行されます。
Webアプリから返されるページのソースをブラウザの機能で見ると
<script>alert("Hello world")</script>
scriptタグ自体です。
これはWebブラウザが実行してしまいますね。
alert("Hello world")
自体は無害ですが、ここにユーザの入力した文字列が来てしまう場合を考えると(悪意のあるコードが実行されるわけなので)ゾッとします😨
エスケープしよう!
HttpResponseにHTMLやJavaScriptコードが渡る可能性がある場合、XSSの対策はエスケープです2。
先ほどの例に適用すると
# example/views.py from django.http import HttpResponse from django.utils.html import escape def example(request): return HttpResponse(escape('<script>alert("Hello world")</script>'))
scriptタグは実行されずに画面に表示されます!
ページのソースは
<script>alert("Hello world")</script>
のようにエスケープされています!
TemplateResponse
Djangoのテンプレートを使う場合、JavaScriptのコードは実行されません。
Djangoのテンプレートでは{{ variable }}
による変数展開でエスケープされるためです。
# example/views.py from django.template.response import TemplateResponse def example(request): context = {"message": '<script>alert("XSSです")</script>'} return TemplateResponse(request, "example/index.html", context)
<!-- example/templates/example/index.html -->
{{ message }}
ページのソースでは、エスケープされています!
<script>alert("Hello world")</script>
Djangoのテンプレートとエスケープ
私はdjango-admin startproject
で生成したsettings.pyを使っているのですが、TEMPLATES
という設定値があります。
生成したsettings.pyでは、django.template.backends.django.DjangoTemplates
が指定されています。
'autoescape': a boolean that controls whether HTML autoescaping is enabled.
It defaults to True.
DjangoTemplates
のautoescape引数のデフォルト値がTrue
です。
これによってエスケープされているわけですね!
出来心でsettings.pyの"OPTIONS"
に"autoescape": False
という組を追加したところ、XSSされました。
他にもエスケープを無効にするタグがあるようです。
ドキュメントの中の「自動エスケープをオフにする」より
https://docs.djangoproject.com/ja/4.2/ref/templates/language/#how-to-turn-it-off
サイト単位やテンプレート単位、あるいは変数単位でデータの自動エスケープ機能を無効にしたい場合には、いくつかの方法があります。
django.shortcutsのrenderを使う場合
https://github.com/django/django/blob/4.2.5/django/shortcuts.py#L17-L25
renderの返り値はHttpResponse
ですが、django.template.loader.render_to_string
の返り値から作られます。
これによりエスケープされた文字列からHttpResponse
が作られているので、XSSは対策済みです。
% python manage.py shell (InteractiveConsole) >>> from django.template import loader >>> context = {"message": '<script>alert("Hello world")</script>'} >>> loader.render_to_string("example/index.html", context, None, using=None) '<script>alert("Hello world")</script>\n'
Djangoテンプレートの落とし穴
ここまでの内容からDjangoテンプレートを使っていれば常にXSSが対策されていると思われるかもしれませんが、それは結論を急ぎすぎみたいです。
ドキュメントの例がこちら3:
https://docs.djangoproject.com/ja/4.2/topics/security/#cross-site-scripting-xss-protection
Django は HTML に対して特に危険とみなされる 特定の文字列のエスケープ を行います。この防御によってユーザーは大半の悪意のある入力から守られますが、いつも簡単で容易に利用できる訳ではありません。たとえば、次に示す例では防御がなされません。
<style class={{ var }}>...</style>
もし var の値が 'class1 onmouseover=javascript:func()' にセットされた場合、不完全な HTML をそのブラウザがどのようにレンダリングするかによっては、許可されていない Javascript を実行させる事になります。(このケースであれば属性値のクオートを行えば対処できます)
<style class="{{ var }}">...</style>
属性値が囲まれていればonmouseover
という別の属性を注入されることはなくなりますね。
Djangoテンプレートのエスケープ機能でXSSを封じられます
終わりに
DjangoにおけるXSS対策の理解の第一歩として、レスポンスにJavaScriptコードを注入できるか手を動かしてみました。
HttpResponse
はJavaScriptコードを注入できる- ユーザの入力から作るような場合は
escape
する!!
- ユーザの入力から作るような場合は
TemplateResponse
ではJavaScriptコードを注入できない(エスケープ済み)
Django Girls TutorialでDjangoを書き始めたのですが、テンプレートやrender関数をまず知ることで、XSSの落とし穴を回避するように巧妙にガイドされていたわけですね。
Webアプリ開発にはXSSという大きな落とし穴がいくつも空いているという印象ですが、その中を縫うようにDjangoは初学者でも一定安全なWebアプリ開発を提供していたのです、すごい!
今回手を動かしたアプリ(XSS脆弱性あり)はこちらです