mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-07-22 12:52:29 +00:00
111 lines
3.8 KiB
Python
111 lines
3.8 KiB
Python
"""Unit tests for ``lib/safepath.py::safe_join``.
|
|
|
|
The three archive extractors and the filename-bound HTTP routes all rely on
|
|
this helper for containment. Pin the contract here so any regression surfaces
|
|
at the helper layer before it can sneak into a call site."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from safepath import safe_join
|
|
|
|
|
|
@pytest.fixture()
|
|
def root(tmp_path):
|
|
return tmp_path
|
|
|
|
|
|
# ── Rejected inputs ───────────────────────────────────────────────────────────
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"name",
|
|
[
|
|
"", # empty
|
|
"..", # bare dotdot
|
|
"../escaped", # one-level traversal
|
|
"../" * 6 + "tmp/pwn", # deep traversal
|
|
"subdir/../../escaped", # mid-path traversal
|
|
"/etc/passwd", # absolute POSIX
|
|
"..\\windows", # backslash-traversal (Windows-style)
|
|
"\x00null", # NUL byte → ValueError on resolve
|
|
],
|
|
)
|
|
def test_safe_join_rejects(root, name):
|
|
assert safe_join(root, name) is None
|
|
|
|
|
|
# ── Accepted inputs ───────────────────────────────────────────────────────────
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"name",
|
|
[
|
|
"file.txt",
|
|
"subdir/file.txt",
|
|
"deep/nested/dir/file.txt",
|
|
"a/./b", # current-dir segment, contained
|
|
"%2e%2e/x", # literal percent-encoding (not decoded)
|
|
],
|
|
)
|
|
def test_safe_join_accepts(root, name):
|
|
result = safe_join(root, name)
|
|
assert result is not None
|
|
assert result.is_relative_to(root.resolve())
|
|
|
|
|
|
def test_safe_join_dot_resolves_to_root(root):
|
|
"""A bare ``.`` is contained (equals root). Caller is responsible for
|
|
declining to write a file at the root path."""
|
|
result = safe_join(root, ".")
|
|
assert result == root.resolve()
|
|
|
|
|
|
def test_safe_join_returns_resolved_path(root):
|
|
"""Result is fully resolved — caller can use ``is_relative_to`` directly
|
|
without re-resolving."""
|
|
(root / "sub").mkdir()
|
|
result = safe_join(root, "sub/file.txt")
|
|
assert result == (root / "sub" / "file.txt").resolve()
|
|
|
|
|
|
def _symlink_or_skip(link: Path, target: Path) -> None:
|
|
"""Create ``link`` pointing at ``target``, or skip the test on platforms
|
|
where symlinks aren't allowed (Windows without dev-mode, restricted
|
|
container, non-symlink-capable filesystem).
|
|
|
|
Passes ``target_is_directory=target.is_dir()`` so directory symlinks work
|
|
on Windows hosts that *do* allow symlinks — otherwise this helper would
|
|
skip unnecessarily on Windows even when the underlying capability is
|
|
present.
|
|
"""
|
|
try:
|
|
link.symlink_to(target, target_is_directory=target.is_dir())
|
|
except (OSError, NotImplementedError) as e:
|
|
pytest.skip(f"symlinks not available on this platform: {e}")
|
|
|
|
|
|
def test_safe_join_symlinked_root_still_contains(tmp_path):
|
|
"""Containment uses realpath on both sides so a symlinked root doesn't
|
|
break the check."""
|
|
real = tmp_path / "real"
|
|
real.mkdir()
|
|
link = tmp_path / "link"
|
|
_symlink_or_skip(link, real)
|
|
result = safe_join(link, "file.txt")
|
|
assert result is not None
|
|
assert result == (real / "file.txt").resolve()
|
|
|
|
|
|
def test_safe_join_traversal_through_symlinked_root(tmp_path):
|
|
"""A ``..`` from a symlinked root must still resolve outside the real
|
|
root (not just the symlink path) and be rejected."""
|
|
real = tmp_path / "real"
|
|
real.mkdir()
|
|
link = tmp_path / "link"
|
|
_symlink_or_skip(link, real)
|
|
assert safe_join(link, "../escaped") is None
|