Skip to content

Commit f8fbcae

Browse files
fix: harden tar extraction path validation
1 parent 7444009 commit f8fbcae

2 files changed

Lines changed: 41 additions & 7 deletions

File tree

‎sagemaker-core/src/sagemaker/core/common_utils.py‎

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1747,7 +1747,11 @@ def _is_bad_path(path, base):
17471747
bool: True if the path is not rooted under the base directory, False otherwise.
17481748
"""
17491749
# joinpath will ignore base if path is absolute
1750-
return not _get_resolved_path(joinpath(base, path)).startswith(base)
1750+
resolved = _get_resolved_path(joinpath(base, path))
1751+
try:
1752+
return os.path.commonpath([resolved, base]) != base
1753+
except ValueError:
1754+
return True
17511755

17521756

17531757
def _is_bad_link(info, base):
@@ -1767,19 +1771,18 @@ def _is_bad_link(info, base):
17671771
return _is_bad_path(info.linkname, base=tip)
17681772

17691773

1770-
def _get_safe_members(members):
1774+
def _get_safe_members(members, base):
17711775
"""A generator that yields members that are safe to extract.
17721776
17731777
It filters out bad paths and bad links.
17741778
17751779
Args:
17761780
members (list): A list of members to check.
1781+
base (str): The base directory for extraction.
17771782
17781783
Yields:
17791784
tarfile.TarInfo: The tar file info.
17801785
"""
1781-
base = _get_resolved_path("")
1782-
17831786
for file_info in members:
17841787
if _is_bad_path(file_info.name, base):
17851788
logger.error("%s is blocked (illegal path)", file_info.name)
@@ -1810,15 +1813,23 @@ def _validate_extracted_paths(extract_path):
18101813
for dir_name in dirs:
18111814
dir_path = os.path.join(root, dir_name)
18121815
resolved = _get_resolved_path(dir_path)
1813-
if not resolved.startswith(base):
1816+
try:
1817+
is_within_base = os.path.commonpath([resolved, base]) == base
1818+
except ValueError:
1819+
is_within_base = False
1820+
if not is_within_base:
18141821
logger.error("Extracted directory escaped extraction path: %s", dir_path)
18151822
raise ValueError(f"Extracted path outside expected directory: {dir_path}")
18161823

18171824
# Check files
18181825
for file_name in files:
18191826
file_path = os.path.join(root, file_name)
18201827
resolved = _get_resolved_path(file_path)
1821-
if not resolved.startswith(base):
1828+
try:
1829+
is_within_base = os.path.commonpath([resolved, base]) == base
1830+
except ValueError:
1831+
is_within_base = False
1832+
if not is_within_base:
18221833
logger.error("Extracted file escaped extraction path: %s", file_path)
18231834
raise ValueError(f"Extracted path outside expected directory: {file_path}")
18241835

@@ -1842,7 +1853,8 @@ def custom_extractall_tarfile(tar, extract_path):
18421853
if hasattr(tarfile, "data_filter"):
18431854
tar.extractall(path=extract_path, filter="data")
18441855
else:
1845-
tar.extractall(path=extract_path, members=_get_safe_members(tar))
1856+
base = _get_resolved_path(extract_path)
1857+
tar.extractall(path=extract_path, members=_get_safe_members(tar.getmembers(), base))
18461858
# Re-validate extracted paths to catch symlink race conditions
18471859
_validate_extracted_paths(extract_path)
18481860

‎sagemaker-core/tests/unit/test_common_utils.py‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from __future__ import absolute_import
1515

1616
import pytest
17+
import io
1718
import time
1819
import tempfile
1920
import os
@@ -1227,6 +1228,27 @@ def test_custom_extractall_tarfile_basic(self, tmp_path):
12271228

12281229
assert (extract_path / "file.txt").exists()
12291230

1231+
def test_custom_extractall_tarfile_blocks_sibling_prefix_escape(self, tmp_path, monkeypatch):
1232+
"""Test fallback extraction blocks sibling paths with the same directory prefix."""
1233+
from sagemaker.core.common_utils import custom_extractall_tarfile
1234+
1235+
monkeypatch.delattr(tarfile, "data_filter", raising=False)
1236+
extract_path = tmp_path / "src"
1237+
extract_path.mkdir()
1238+
escaped_path = tmp_path / "src_evil" / "escaped.txt"
1239+
stream = io.BytesIO()
1240+
with tarfile.open(fileobj=stream, mode="w") as tar:
1241+
data = b"outside"
1242+
file_info = tarfile.TarInfo(name="../src_evil/escaped.txt")
1243+
file_info.size = len(data)
1244+
tar.addfile(file_info, io.BytesIO(data))
1245+
stream.seek(0)
1246+
1247+
with tarfile.open(fileobj=stream, mode="r") as tar:
1248+
custom_extractall_tarfile(tar, str(extract_path))
1249+
1250+
assert not escaped_path.exists()
1251+
12301252

12311253
class TestCanModelPackageSourceUriAutopopulate:
12321254
"""Test can_model_package_source_uri_autopopulate function."""

0 commit comments

Comments
 (0)