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