Skip to content

Commit

Permalink
convert all input paths to relative to FileSystem.root
Browse files Browse the repository at this point in the history
  • Loading branch information
e3krisztian committed Aug 8, 2023
1 parent 18b3fd1 commit 96367e1
Show file tree
Hide file tree
Showing 6 changed files with 257 additions and 198 deletions.
201 changes: 97 additions & 104 deletions tests/test_file_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
FileSystem,
InvalidInputFormat,
StructParser,
chop_root,
convert_int8,
convert_int16,
convert_int32,
Expand Down Expand Up @@ -364,61 +365,58 @@ def test_get_endian_resets_the_file_pointer(self):
assert file.tell() == pos


class TestFileSystem:
@pytest.mark.parametrize(
"path, expected",
[
("/unblob/sandbox/file", True),
("/unblob/sandbox/dir/file", True),
("relative/path/file", False),
("/unblob/sandbox/../file", False),
("/unblob/sandbox/..", False),
("/etc/passwd", False),
],
)
def test_is_safe_path(self, path, expected):
basedir = Path("/unblob/sandbox")
assert FileSystem(basedir).is_safe_path(Path(path)) is expected
@pytest.mark.parametrize(
"input_path, expected",
[
pytest.param("/", ".", id="absolute-root"),
pytest.param("/path/to/file", "path/to/file", id="absolute-path"),
pytest.param(".", ".", id="current-directory"),
pytest.param("path/to/file", "path/to/file", id="relative-path"),
],
)
def test_chop_root(input_path: str, expected: str):
assert chop_root(Path(input_path)) == Path(expected)


class TestFileSystem:
@pytest.mark.parametrize(
"path",
[
"/unblob/sandbox/file",
"/unblob/sandbox/some/dir/file",
"/unblob/sandbox/some/dir/../file",
"/unblob/sandbox/some/dir/../../file",
"/etc/passwd",
"file",
"some/dir/file",
"some/dir/../file",
"some/dir/../../file",
],
)
def test_check_path_success(self, path):
def test_get_checked_path_success(self, path):
fs = FileSystem(Path("/unblob/sandbox"))
assert fs.check_path(Path(path), "test")
checked_path = fs.get_checked_path(Path(path), "test")
assert checked_path
assert fs.problems == []
assert checked_path.relative_to(fs.root)

@pytest.mark.parametrize(
"path",
[
"/unblob/sandbox/../file",
"/unblob/sandbox/some/dir/../../../file",
"/unblob/sandbox/some/dir/../../../",
"/unblob/sandbox/some/dir/../../..",
"../file",
"some/dir/../../../file",
"some/dir/../../../",
"some/dir/../../..",
],
)
def test_check_path_path_traversal_is_reported(self, path):
def test_get_checked_path_path_traversal_is_reported(self, path):
fs = FileSystem(Path("/unblob/sandbox"))
assert not fs.check_path(Path(path), "test")
assert not fs.get_checked_path(Path(path), "test")
assert fs.problems

def test_check_path_relative_path_fail(self):
fs = FileSystem(Path("/unblob/sandbox"))
assert not fs.check_path(Path("relative/path"), "test")

def test_check_path_path_traversal_reports(self):
def test_get_checked_path_path_traversal_reports(self):
fs = FileSystem(Path("/unblob/sandbox"))
op1 = f"test1-{object()}"
op2 = f"test2-{object()}"
assert op1 != op2
assert not fs.check_path(fs.root / "../file", op1)
assert not fs.check_path(Path("/etc/passwd"), op2)
assert not fs.get_checked_path(Path("../file"), op1)
assert not fs.get_checked_path(Path("../etc/passwd"), op2)

report1, report2 = fs.problems

Expand All @@ -428,52 +426,7 @@ def test_check_path_path_traversal_reports(self):

assert "path traversal" in report2.problem
assert op2 in report2.problem
assert report2.path == "../../etc/passwd"

@pytest.mark.parametrize(
"src, dst",
[
pytest.param(
"/unblob/sandbox/file",
"/unblob/sandbox/link",
id="absolute",
),
pytest.param(
"somewhere/inside/file",
"/unblob/sandbox/link",
id="relative",
),
],
)
def test_check_link_success(self, src, dst):
fs = FileSystem(Path("/unblob/sandbox"))
assert fs.check_link(Path(src), Path(dst))
assert fs.problems == []

@pytest.mark.parametrize(
"src, dst",
[
pytest.param(
"/unblob/sandbox/file",
"/etc/passwd",
id="dst-out-of-sandbox",
),
pytest.param(
"../outside",
"/unblob/sandbox/outside",
id="src-out-of-sandbox-relative",
),
pytest.param(
"/unblob/outside",
"/unblob/sandbox/outside",
id="src-out-of-sandbox-absolute",
),
],
)
def test_check_link_failure(self, src, dst):
fs = FileSystem(Path("/unblob/sandbox"))
assert not fs.check_link(Path(src), Path(dst))
assert fs.problems
assert report2.path == "../etc/passwd"

@pytest.fixture
def sandbox_parent(self, tmp_path: Path):
Expand All @@ -490,68 +443,108 @@ def sandbox(self, sandbox_root: Path):

def test_carve(self, sandbox: FileSystem):
file = File.from_bytes(b"0123456789")
output_path = sandbox.root / "carved"
sandbox.carve(output_path, file, 1, 2)
sandbox.carve(Path("carved"), file, 1, 2)

assert output_path.read_bytes() == b"12"
assert (sandbox.root / "carved").read_bytes() == b"12"
assert sandbox.problems == []

def test_carve_outside_sandbox(self, sandbox: FileSystem):
file = File.from_bytes(b"0123456789")
output_path = sandbox.root / "../carved"
sandbox.carve(output_path, file, 1, 2)
sandbox.carve(Path("../carved"), file, 1, 2)

assert not output_path.exists()
assert not (sandbox.root / "../carved").exists()
assert sandbox.problems

def test_mkdir(self, sandbox: FileSystem):
output_path = sandbox.root / "directory"
sandbox.mkdir(output_path)
sandbox.mkdir(Path("directory"))

assert output_path.is_dir()
assert (sandbox.root / "directory").is_dir()
assert sandbox.problems == []

def test_mkdir_outside_sandbox(self, sandbox: FileSystem):
output_path = sandbox.root / "../directory"
sandbox.mkdir(output_path)
sandbox.mkdir(Path("../directory"))

assert not output_path.exists()
assert not (sandbox.root / "../directory").exists()
assert sandbox.problems

def test_mkfifo(self, sandbox: FileSystem):
output_path = sandbox.root / "named_pipe"
sandbox.mkfifo(output_path)
sandbox.mkfifo(Path("named_pipe"))

assert output_path.is_fifo()
assert (sandbox.root / "named_pipe").is_fifo()
assert sandbox.problems == []

def test_mkfifo_outside_sandbox(self, sandbox: FileSystem):
output_path = sandbox.root / "../named_pipe"
sandbox.mkfifo(output_path)
sandbox.mkfifo(Path("../named_pipe"))

assert not output_path.exists()
assert not (sandbox.root / "../named_pipe").exists()
assert sandbox.problems

def test_create_symlink(self, sandbox: FileSystem):
output_path = sandbox.root / "symlink"
sandbox.create_symlink(Path("target file"), output_path)
sandbox.create_symlink(Path("target file"), Path("symlink"))

output_path = sandbox.root / "symlink"
assert not output_path.exists()
assert os.readlink(output_path) == "target file"
assert sandbox.problems == []

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"))

output_path = sandbox.root / "symlink"
assert output_path.exists()
assert os.readlink(output_path) == "target file"
assert sandbox.problems == []

def test_create_symlink_absolute_paths_self_referenced(self, sandbox: FileSystem):
sandbox.mkdir(Path("/etc"))
sandbox.create_symlink(Path("/etc/passwd"), Path("/etc/passwd"))

output_path = sandbox.root / "etc/passwd"
assert not output_path.exists()
assert os.readlink(output_path) == "etc/passwd"
assert sandbox.problems == []

def test_create_symlink_outside_sandbox(self, sandbox: FileSystem):
output_path = sandbox.root / "../symlink"
sandbox.create_symlink(Path("target file"), output_path)
sandbox.create_symlink(Path("target file"), Path("../symlink"))

output_path = sandbox.root / "../symlink"
assert not os.path.lexists(output_path)
assert sandbox.problems

def test_create_symlink_path_traversal(
self, sandbox: FileSystem, sandbox_parent: Path
):
"""Document a remaining path traversal scenario through a symlink chain.
unblob.extractor.fix_symlinks() exists to cover up cases like this.
"""
(sandbox_parent / "outer-secret").write_text("private key")

# The path traversal is possible because at the creation of "secret" "future" does not exist
# so it is not yet possible to determine if it will be a symlink to be allowed or not.
# When the order of the below 2 lines are changed, the path traversal is recognized and prevented.
sandbox.create_symlink(Path("future/../outer-secret"), Path("secret"))
sandbox.create_symlink(Path("."), Path("future"))

assert sandbox.problems == []
assert (sandbox.root / "secret").read_text() == "private key"

def test_create_hardlink(self, sandbox: FileSystem):
output_path = sandbox.root / "hardlink"
linked_file = sandbox.root / "file"
linked_file.write_bytes(b"")
sandbox.create_hardlink(linked_file, output_path)
sandbox.create_hardlink(Path("file"), Path("hardlink"))

assert output_path.stat().st_nlink == 2
assert output_path.stat().st_ino == linked_file.stat().st_ino
assert sandbox.problems == []

def test_create_hardlink_absolute_paths(self, sandbox: FileSystem):
output_path = sandbox.root / "hardlink"
linked_file = sandbox.root / "file"
linked_file.write_bytes(b"")
sandbox.create_hardlink(Path("/file"), Path("/hardlink"))

assert output_path.stat().st_nlink == 2
assert output_path.stat().st_ino == linked_file.stat().st_ino
Expand All @@ -561,7 +554,7 @@ def test_create_hardlink_outside_sandbox(self, sandbox: FileSystem):
output_path = sandbox.root / "../hardlink"
linked_file = sandbox.root / "file"
linked_file.write_bytes(b"")
sandbox.create_hardlink(linked_file, output_path)
sandbox.create_hardlink(Path("file"), Path("../hardlink"))

assert not os.path.lexists(output_path)
assert sandbox.problems
Loading

0 comments on commit 96367e1

Please sign in to comment.