Skip to content

Commit b8de2e0

Browse files
committed
fix(utils): harden zip member path exclusion checks
1 parent a35072d commit b8de2e0

2 files changed

Lines changed: 20 additions & 2 deletions

File tree

weblate/utils/files.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import os
77
import shutil
88
import stat
9-
from pathlib import Path
9+
from pathlib import Path, PurePosixPath, PureWindowsPath
1010
from typing import TYPE_CHECKING
1111

1212
from django.conf import settings
@@ -98,7 +98,16 @@ def should_skip(location):
9898

9999
def is_excluded(path: str) -> bool:
100100
"""Whether path should be excluded from zip extraction."""
101-
return any(exclude in f"/{path}/" for exclude in PATH_EXCLUDES) or ".." in path
101+
normalized = path.replace("\\", "/")
102+
posix_path = PurePosixPath(normalized)
103+
windows_path = PureWindowsPath(path)
104+
return (
105+
any(exclude in f"/{normalized}/" for exclude in PATH_EXCLUDES)
106+
or ".." in posix_path.parts
107+
or posix_path.is_absolute()
108+
or windows_path.is_absolute()
109+
or bool(windows_path.drive)
110+
)
102111

103112

104113
def is_path_within_directory(path: str, directory: str) -> bool:

weblate/utils/tests/test_files.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from weblate.utils.files import (
1818
REPO_TEMP_DIRNAME,
1919
get_repo_temp_dir,
20+
is_excluded,
2021
is_path_within_directory,
2122
read_file_bytes,
2223
remove_tree,
@@ -71,6 +72,14 @@ def test_read_file_bytes_resets_position_to_start(self) -> None:
7172
self.assertEqual(read_file_bytes(filelike, max_size=10), b"test")
7273
self.assertEqual(filelike.tell(), 0)
7374

75+
def test_is_excluded_rejects_path_traversal_and_absolute_paths(self) -> None:
76+
self.assertTrue(is_excluded("../outside.po"))
77+
self.assertTrue(is_excluded("/etc/passwd"))
78+
self.assertTrue(is_excluded(r"C:\temp\escape.po"))
79+
80+
def test_is_excluded_allows_regular_relative_paths(self) -> None:
81+
self.assertFalse(is_excluded("locale/cs/messages.po"))
82+
7483
def test_is_path_within_directory_accepts_descendants(self) -> None:
7584
with tempfile.TemporaryDirectory() as tempdir:
7685
repo_path = os.path.join(tempdir, "repo")

0 commit comments

Comments
 (0)