From b865009d414a0f6fd0c0f5ad7434b2c13eb761c7 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Fri, 7 Sep 2012 16:49:22 -0400 Subject: [PATCH] Fixed #12397 -- allow safe_join to work with the root file system path, which means you can have your root template or file upload path at this location. You almost certainly don't want to do this, except in *very* limited sandboxed situations. --- django/utils/_os.py | 17 ++++++++++------- tests/regressiontests/utils/os_utils.py | 21 +++++++++++++++++++++ tests/regressiontests/utils/tests.py | 1 + 3 files changed, 32 insertions(+), 7 deletions(-) create mode 100644 tests/regressiontests/utils/os_utils.py diff --git a/django/utils/_os.py b/django/utils/_os.py index 9eb5e5e8ea..1ea12aed8a 100644 --- a/django/utils/_os.py +++ b/django/utils/_os.py @@ -1,6 +1,6 @@ import os import stat -from os.path import join, normcase, normpath, abspath, isabs, sep +from os.path import join, normcase, normpath, abspath, isabs, sep, dirname from django.utils.encoding import force_text from django.utils import six @@ -41,13 +41,16 @@ def safe_join(base, *paths): paths = [force_text(p) for p in paths] final_path = abspathu(join(base, *paths)) base_path = abspathu(base) - base_path_len = len(base_path) # Ensure final_path starts with base_path (using normcase to ensure we - # don't false-negative on case insensitive operating systems like Windows) - # 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 normcase(final_path).startswith(normcase(base_path)) \ - or final_path[base_path_len:base_path_len+1] not in ('', sep): + # don't false-negative on case insensitive operating systems like Windows), + # further, one of the following conditions must be true: + # a) The next character is the path separator (to prevent conditions like + # safe_join("/dir", "/../d")) + # b) The final path must be the same as the base path. + # c) The base path must be the most root path (meaning either "/" or "C:\\") + if (not normcase(final_path).startswith(normcase(base_path + sep)) and + normcase(final_path) != normcase(base_path) and + dirname(normcase(base_path)) != normcase(base_path)): raise ValueError('The joined path (%s) is located outside of the base ' 'path component (%s)' % (final_path, base_path)) return final_path diff --git a/tests/regressiontests/utils/os_utils.py b/tests/regressiontests/utils/os_utils.py new file mode 100644 index 0000000000..a78f348cf5 --- /dev/null +++ b/tests/regressiontests/utils/os_utils.py @@ -0,0 +1,21 @@ +from django.utils import unittest +from django.utils._os import safe_join + + +class SafeJoinTests(unittest.TestCase): + def test_base_path_ends_with_sep(self): + self.assertEqual( + safe_join("/abc/", "abc"), + "/abc/abc", + ) + + def test_root_path(self): + self.assertEqual( + safe_join("/", "path"), + "/path", + ) + + self.assertEqual( + safe_join("/", ""), + "/", + ) diff --git a/tests/regressiontests/utils/tests.py b/tests/regressiontests/utils/tests.py index f4fa75b177..061c669eb7 100644 --- a/tests/regressiontests/utils/tests.py +++ b/tests/regressiontests/utils/tests.py @@ -21,6 +21,7 @@ from .http import TestUtilsHttp from .ipv6 import TestUtilsIPv6 from .jslex import JsToCForGettextTest, JsTokensTest from .module_loading import CustomLoader, DefaultLoader, EggLoader +from .os_utils import SafeJoinTests from .regex_helper import NormalizeTests from .simplelazyobject import TestUtilsSimpleLazyObject from .termcolors import TermColorTests