社内で話題になったのでまとめる。Djangoとディレクトリトラバーサルについて。検証したバージョンは1.2.1。
ユーザの入力値などを利用してファイルパスなどを生成したりする場合やファイルをアップロードさせる場合には特に注意する。
DjangoのテンプレートやストレージAPIでは、ディレクトリトラバーサルを避けるような工夫がある。
os.path.joinに注意
よくあるパターンで、 "../" がパスに入っていると、意図しないファイルを参照するようになっていないかどうか。Windowsの場合には、パスのセパレータはバックスラッシュも使えることに注意する。
危なそうな場合は、 django.utils._os.safe_join などを利用する。
>>> BASE_DIR = '/home/tokibito/templates/' >>> import os >>> os.path.join(BASE_DIR, '../test.html') # NG '/home/tokibito/templates/../test.html' >>> from django.utils._os import safe_join >>> safe_join(BASE_DIR, '../test.html') # OK ------------------------------------------------------------ Traceback (most recent call last): File "<ipython console>", line 1, in <module> File "/home/tokibito/.virtualenvs/sandbox/lib/python2.5/site-packages/django/utils/_os.py", line 44, in safe_join raise ValueError('the joined path is located outside of the base path' ValueError: the joined path is located outside of the base path component
テンプレート
django.template.loaders.filesystem.Loaderはsafe_joinを利用しているので、テンプレートディレクトリ以外を参照しようとした場合にはTemplateDoesNotExistエラーとなる。
ストレージAPI
django.core.files.storage.FileSystemStorageはsafe_joinを利用しているので、locationの外側にアクセスしようとするとSuspiciousOperationエラーとなる。
>>> from django.core.files.storage import FileSystemStorage >>> storage = FileSystemStorage(location='/home/tokibito/uploads/') >>> f = storage.open('../test.html') ------------------------------------------------------------ Traceback (most recent call last): File "<ipython console>", line 1, in <module> File "/home/tokibito/.virtualenvs/sandbox/lib/python2.5/site-packages/django/core/files/storage.py", line 32, in open file = self._open(name, mode) File "/home/tokibito/.virtualenvs/sandbox/lib/python2.5/site-packages/django/core/files/storage.py", line 137, in _open return File(open(self.path(name), mode)) File "/home/tokibito/.virtualenvs/sandbox/lib/python2.5/site-packages/django/core/files/storage.py", line 212, in path raise SuspiciousOperation("Attempted access to '%s' denied." % name) SuspiciousOperation: Attempted access to '../test.html' denied.
ダメな例
ファイルパスにユーザの入力値などが含まれるもので、openで開いたり、そのままのパス名でファイル操作したりするなど。
urls.py
# coding:utf-8 from django.conf.urls.defaults import * from django.http import HttpResponse import os FILES_DIR = '/home/tokibito/myproject/files/' def serve_text(request, filename): """ テキストファイルを読み込んで返す """ path = os.path.normpath(os.path.join(FILES_DIR, filename)) f = open(path, 'r') return HttpResponse(f.read(), content_type='text/plain') urlpatterns = patterns('', (r'^(?P<filename>.*\.txt)$', serve_text), )
例えば上記のコードで、 /home/tokibito/test.txt を作成している場合には、
$ curl http://example.com/../../test.txt
このようにcurlでアクセスすると意図しないファイルを見れてしまう。これ以外にもいろいろなパターンはあると思うので注意する。サードパーティ製のモジュールなどを使っている場合でもこういった挙動をとっていないかは、確認しておいたほうが良い。