Fixed #4952 -- Fixed the `get_template_sources` functions of the `app_directories` and `filesystem` template loaders to not return paths outside of given template directories. Both functions now make use of a new `safe_join` utility function. Thanks to SmileyChris for help with the patch.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@5750 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Gary Wilson Jr 2007-07-23 04:45:01 +00:00
parent 7a16a1d81a
commit 304381616f
4 changed files with 93 additions and 9 deletions

View File

@ -1,9 +1,14 @@
# Wrapper for loading templates from "template" directories in installed app packages. """
Wrapper for loading templates from "template" directories in INSTALLED_APPS
packages.
"""
import os
from django.conf import settings from django.conf import settings
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.template import TemplateDoesNotExist from django.template import TemplateDoesNotExist
import os from django.utils._os import safe_join
# At compile time, cache the directories to search. # At compile time, cache the directories to search.
app_template_dirs = [] app_template_dirs = []
@ -28,8 +33,14 @@ for app in settings.INSTALLED_APPS:
app_template_dirs = tuple(app_template_dirs) app_template_dirs = tuple(app_template_dirs)
def get_template_sources(template_name, template_dirs=None): def get_template_sources(template_name, template_dirs=None):
for template_dir in app_template_dirs: if not template_dirs:
yield os.path.join(template_dir, template_name) template_dirs = app_template_dirs
for template_dir in template_dirs:
try:
yield safe_join(template_dir, template_name)
except ValueError:
# The joined path was located outside of template_dir.
pass
def load_template_source(template_name, template_dirs=None): def load_template_source(template_name, template_dirs=None):
for filepath in get_template_sources(template_name, template_dirs): for filepath in get_template_sources(template_name, template_dirs):

View File

@ -1,14 +1,20 @@
# Wrapper for loading templates from the filesystem. """
Wrapper for loading templates from the filesystem.
"""
from django.conf import settings from django.conf import settings
from django.template import TemplateDoesNotExist from django.template import TemplateDoesNotExist
import os from django.utils._os import safe_join
def get_template_sources(template_name, template_dirs=None): def get_template_sources(template_name, template_dirs=None):
if not template_dirs: if not template_dirs:
template_dirs = settings.TEMPLATE_DIRS template_dirs = settings.TEMPLATE_DIRS
for template_dir in template_dirs: for template_dir in template_dirs:
yield os.path.join(template_dir, template_name) try:
yield safe_join(template_dir, template_name)
except ValueError:
# The joined path was located outside of template_dir.
pass
def load_template_source(template_name, template_dirs=None): def load_template_source(template_name, template_dirs=None):
tried = [] tried = []

23
django/utils/_os.py Normal file
View File

@ -0,0 +1,23 @@
from os.path import join, normcase, abspath, sep
def safe_join(base, *paths):
"""
Join one or more path components to the base path component intelligently.
Return a normalized, absolute version of the final path.
The final path must be located inside of the base path component (otherwise
a ValueError is raised).
"""
# We need to use normcase to ensure we don't false-negative on case
# insensitive operating systems (like Windows).
final_path = normcase(abspath(join(base, *paths)))
base_path = normcase(abspath(base))
base_path_len = len(base_path)
# Ensure final_path starts with base_path and that the next character after
# the final path is os.sep (or nothing, in which case final_path must be
# equal to base_path).
if not final_path.startswith(base_path) \
or final_path[base_path_len:base_path_len+1] not in ('', sep):
raise ValueError('the joined path is located outside of the base path'
' component')
return final_path

View File

@ -6,13 +6,17 @@ if __name__ == '__main__':
# before importing 'template'. # before importing 'template'.
settings.configure() settings.configure()
import os
import unittest
from datetime import datetime, timedelta
from django import template from django import template
from django.template import loader from django.template import loader
from django.template.loaders import app_directories, filesystem
from django.utils.translation import activate, deactivate, install, ugettext as _ from django.utils.translation import activate, deactivate, install, ugettext as _
from django.utils.tzinfo import LocalTimezone from django.utils.tzinfo import LocalTimezone
from datetime import datetime, timedelta
from unicode import unicode_tests from unicode import unicode_tests
import unittest
# Some other tests we would like to run # Some other tests we would like to run
__test__ = { __test__ = {
@ -75,6 +79,46 @@ class UTF8Class:
return u'ŠĐĆŽćžšđ'.encode('utf-8') return u'ŠĐĆŽćžšđ'.encode('utf-8')
class Templates(unittest.TestCase): class Templates(unittest.TestCase):
def test_loaders_security(self):
def test_template_sources(path, template_dirs, expected_sources):
# Fix expected sources so they are normcased and abspathed
expected_sources = [os.path.normcase(os.path.abspath(s)) for s in expected_sources]
# Test app_directories loader
sources = app_directories.get_template_sources(path, template_dirs)
self.assertEqual(list(sources), expected_sources)
# Test filesystem loader
sources = filesystem.get_template_sources(path, template_dirs)
self.assertEqual(list(sources), expected_sources)
template_dirs = ['/dir1', '/dir2']
test_template_sources('index.html', template_dirs,
['/dir1/index.html', '/dir2/index.html'])
test_template_sources('/etc/passwd', template_dirs,
[])
test_template_sources('etc/passwd', template_dirs,
['/dir1/etc/passwd', '/dir2/etc/passwd'])
test_template_sources('../etc/passwd', template_dirs,
[])
test_template_sources('../../../etc/passwd', template_dirs,
[])
test_template_sources('/dir1/index.html', template_dirs,
['/dir1/index.html'])
test_template_sources('../dir2/index.html', template_dirs,
['/dir2/index.html'])
test_template_sources('/dir1blah', template_dirs,
[])
test_template_sources('../dir1blah', template_dirs,
[])
# Case insensitive tests (for win32). Not run unless we're on
# a case insensitive operating system.
if os.path.normcase('/TEST') == os.path.normpath('/test'):
template_dirs = ['/dir1', '/DIR2']
test_template_sources('index.html', template_dirs,
['/dir1/index.html', '/dir2/index.html'])
test_template_sources('/DIR1/index.HTML', template_dirs,
['/dir1/index.html'])
def test_templates(self): def test_templates(self):
# NOW and NOW_tz are used by timesince tag tests. # NOW and NOW_tz are used by timesince tag tests.
NOW = datetime.now() NOW = datetime.now()