Skip to content

Commit 9ee4a7c

Browse files
committed
Fix: SafeTarExtractor path containment check can be bypassed due to naive string prefix matching
1 parent 8cc3e31 commit 9ee4a7c

2 files changed

Lines changed: 35 additions & 21 deletions

File tree

security/tar_safe.py

Lines changed: 14 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -130,30 +130,24 @@ def _validate_member(
130130

131131
target_path = self._get_safe_path(member, extract_path)
132132

133-
# Check for path traversal
133+
# Check for path traversal using Path.relative_to()
134134
try:
135135
target_abs = target_path.resolve()
136136
extract_abs = extract_path.resolve()
137-
if os.path.commonpath([str(target_abs), str(extract_abs)]) != str(extract_abs):
138-
raise UnsafeTarError(
139-
f"Path traversal detected: {member.name} -> {target_path}"
140-
)
141-
except (OSError, RuntimeError):
142-
target_str = str(target_path.absolute())
143-
extract_str = str(extract_path.absolute())
144-
try:
145-
if os.path.commonpath([target_str, extract_str]) != extract_str:
146-
raise UnsafeTarError(
147-
f"Path traversal detected: {member.name} -> {target_path}"
148-
)
149-
except ValueError:
150-
raise UnsafeTarError(
151-
f"Path traversal detected (different drives): {member.name} -> {target_path}"
152-
)
153-
except ValueError:
154-
# os.path.commonpath raises ValueError on Windows for different drives
137+
target_abs.relative_to(extract_abs)
138+
except (ValueError, OSError, RuntimeError):
155139
raise UnsafeTarError(
156-
f"Path traversal detected (different drives): {member.name} -> {target_path}"
140+
f"Path traversal detected: {member.name} -> {target_path}"
141+
)
142+
143+
if target_path.exists() and not overwrite:
144+
raise UnsafeTarError(
145+
f"File already exists and overwrite=False: {target_path}"
146+
)
147+
148+
if member.type in self.blocked_types:
149+
raise UnsafeTarError(
150+
f"Blocked file type for {member.name}: {member.type}"
157151
)
158152

159153
if target_path.exists() and not overwrite:

tests/test_security.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,27 @@ def test_sibling_directory_traversal(self, tmp_path):
174174
with pytest.raises(UnsafeTarError):
175175
extractor.extract(tar_file, extract_dir)
176176

177-
def test_absolute_path_traversal(self, tmp_path):
177+
def test_prefix_bypass_path_traversal(self, tmp_path):
178+
"""Test that prefix-based bypass (e.g., /tmp/a vs /tmp/ab) is caught."""
179+
tar_file = tmp_path / "prefix_bypass.tar"
180+
181+
with tarfile.open(tar_file, 'w') as tar:
182+
info = tarfile.TarInfo(name='../ab/evil.txt')
183+
info.size = 10
184+
info.type = tarfile.REGTYPE
185+
content = b'x' * 10
186+
fileobj = io.BytesIO(content)
187+
tar.addfile(info, fileobj)
188+
189+
extract_dir = tmp_path / "a"
190+
extract_dir.mkdir(exist_ok=True)
191+
sibling_dir = tmp_path / "ab"
192+
sibling_dir.mkdir(exist_ok=True)
193+
194+
extractor = SafeTarExtractor()
195+
196+
with pytest.raises(UnsafeTarError):
197+
extractor.extract(tar_file, extract_dir)
178198
"""Test absolute path traversal prevention."""
179199
tar_file = tmp_path / "absolute_traversal.tar"
180200

0 commit comments

Comments
 (0)