Skip to content

Commit

Permalink
Merge pull request #775 from onekey-sec/safe_tar_symlinks_fix
Browse files Browse the repository at this point in the history
Fix SafeTarFile symlinks extraction

Co-authored-by: Andrew Fasano <[email protected]>
  • Loading branch information
e3krisztian and Andrew Fasano authored Feb 19, 2024
2 parents 20077c8 + 600f166 commit 25687f1
Show file tree
Hide file tree
Showing 11 changed files with 64 additions and 9 deletions.
3 changes: 3 additions & 0 deletions tests/integration/archive/tar/__input__/symlinks.tar
Git LFS file not shown
Git LFS file not shown
26 changes: 25 additions & 1 deletion tests/test_file_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
round_down,
round_up,
)
from unblob.report import PathTraversalProblem
from unblob.report import LinkExtractionProblem, PathTraversalProblem


@pytest.mark.parametrize(
Expand Down Expand Up @@ -503,6 +503,30 @@ def test_create_symlink(self, sandbox: FileSystem):
assert os.readlink(output_path) == "target file"
assert sandbox.problems == []

def test_create_symlink_target_inside_sandbox(self, sandbox: FileSystem):
# ./sbin/shell -> ../bin/sh
sandbox.mkdir(Path("bin"))
sandbox.write_bytes(Path("bin/sh"), b"posix shell")
sandbox.mkdir(Path("sbin"))
sandbox.create_symlink(Path("../bin/sh"), Path("sbin/shell"))

output_path = sandbox.root / "sbin/shell"
assert output_path.read_bytes() == b"posix shell"
assert output_path.exists()
assert os.readlink(output_path) == "../bin/sh"
assert sandbox.problems == []

def test_create_symlink_target_outside_sandbox(self, sandbox: FileSystem):
# /shell -> ../bin/sh
sandbox.mkdir(Path("bin"))
sandbox.write_bytes(Path("bin/sh"), b"posix shell")
sandbox.create_symlink(Path("../bin/sh"), Path("/shell"))

assert any(p for p in sandbox.problems if isinstance(p, LinkExtractionProblem))
output_path = sandbox.root / "shell"
assert not output_path.exists()
assert not output_path.is_symlink()

def test_create_symlink_absolute_paths(self, sandbox: FileSystem):
sandbox.write_bytes(Path("target file"), b"test content")
sandbox.create_symlink(Path("/target file"), Path("/symlink"))
Expand Down
32 changes: 26 additions & 6 deletions unblob/handlers/archive/_safe_tarfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def extract(self, tarinfo: tarfile.TarInfo, extract_root: Path): # noqa: C901
"Absolute path.",
"Converted to extraction relative path.",
)
tarinfo.name = f"./{tarinfo.name}"
tarinfo.name = str(Path(tarinfo.name).relative_to("/"))

# prevent traversal attempts through file name
if not is_safe_path(basedir=extract_root, path=extract_root / tarinfo.name):
Expand All @@ -77,16 +77,36 @@ def extract(self, tarinfo: tarfile.TarInfo, extract_root: Path): # noqa: C901
# prevent traversal attempts through links
if tarinfo.islnk() or tarinfo.issym():
if Path(tarinfo.linkname).is_absolute():

def calculate_linkname():
root = extract_root.resolve()
path = (extract_root / tarinfo.name).resolve()

if path.parts[: len(root.parts)] != root.parts:
return None

depth = max(0, len(path.parts) - len(root.parts) - 1)
return ("/".join([".."] * depth) or ".") + tarinfo.linkname

relative_linkname = calculate_linkname()
if relative_linkname is None:
self.record_problem(
tarinfo,
"Absolute path conversion to extraction relative failed - would escape root.",
"Skipped.",
)
return

assert not Path(relative_linkname).is_absolute()
self.record_problem(
tarinfo,
"Absolute path as link target.",
"Converted to extraction relative path.",
)
tarinfo.linkname = f"./{tarinfo.linkname}"
if not is_safe_path(
basedir=extract_root,
path=extract_root / tarinfo.linkname,
):
tarinfo.linkname = relative_linkname

resolved_path = (extract_root / tarinfo.name).parent / tarinfo.linkname
if not is_safe_path(basedir=extract_root, path=resolved_path):
self.record_problem(
tarinfo,
"Traversal attempt through link path.",
Expand Down

0 comments on commit 25687f1

Please sign in to comment.