Djangoとディレクトリトラバーサル

社内で話題になったのでまとめる。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でアクセスすると意図しないファイルを見れてしまう。これ以外にもいろいろなパターンはあると思うので注意する。サードパーティ製のモジュールなどを使っている場合でもこういった挙動をとっていないかは、確認しておいたほうが良い。