Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ Changelog
=========

* Drop Python 3.9 support.
* Fix potential unauthorised file access vulnerability in "autorefesh" mode. See `PR #684 <https://github.com/evansd/whitenoise/pull/684>`__ for details, and a reminder that autorefresh mode has always been documented as unsuitable for production use. Thanks Seth Larson for reporting.

6.11.0 (2025-09-18)
-------------------
Expand Down
11 changes: 10 additions & 1 deletion src/whitenoise/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ def candidate_paths_for_url(self, url):
for root, prefix in self.directories:
if url.startswith(prefix):
path = os.path.join(root, url[len(prefix) :])
if os.path.commonprefix((root, path)) == root:
if self.path_is_child_of(path, root):
yield path

def find_file_at_path(self, path, url):
Expand Down Expand Up @@ -184,6 +184,15 @@ def url_is_canonical(url):
normalised += "/"
return normalised == url

@staticmethod
def path_is_child_of(path, root):
try:
return os.path.commonpath((path, root)) + os.path.sep == root
except ValueError:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When is ValueError raised?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair question! I have amended with some extra comments. Let me know if that makes sense.

# We get a ValueError if `path` and `root` are on different Windows drives:
# https://docs.python.org/3/library/os.path.html#os.path.commonpath
return False

@staticmethod
def is_compressed_variant(path, stat_cache=None):
if path[-3:] in (".gz", ".br"):
Expand Down
19 changes: 19 additions & 0 deletions tests/test_whitenoise.py
Original file line number Diff line number Diff line change
Expand Up @@ -380,3 +380,22 @@ def test_chunked_file_size_matches_range_with_range_header():
while response.file.read(1):
file_size += 1
assert file_size == 14


@pytest.mark.parametrize(
"root,path,expected",
[
("/one/two/", "/one/two/three", True),
("/one/two/", "/one_two/three", False),
# Having different drive letters triggers an exception in `commonpath()` on
# Windows which we should handle gracefully
("A:/some/path", "B:/another/path", False),
# Relative paths also trigger exceptions (it shouldn't be possible to supply
# these but better to handle all cases)
("/one/two/", "two/three", False),
],
)
def test_path_is_child_of(root, path, expected):
root = root.replace("/", os.path.sep)
path = path.replace("/", os.path.sep)
assert WhiteNoise.path_is_child_of(path, root) == expected