diff --git a/src/shotbible/check.py b/src/shotbible/check.py index 76605aa..16163e4 100644 --- a/src/shotbible/check.py +++ b/src/shotbible/check.py @@ -214,10 +214,15 @@ def _listed_refs(bible: Bible) -> set[str]: def _ref_exists(root: Path, ref: str) -> bool: if not str(ref).strip(): return False + root = root.resolve() path = Path(ref) - if path.is_absolute(): - return path.exists() - return (root / ref).exists() + candidate = path if path.is_absolute() else root / ref + try: + resolved = candidate.resolve() + resolved.relative_to(root) + except (ValueError, OSError): + return False + return candidate.exists() def _normalize_look(look: str) -> str: diff --git a/tests/test_ref_escape.py b/tests/test_ref_escape.py new file mode 100644 index 0000000..ac02760 --- /dev/null +++ b/tests/test_ref_escape.py @@ -0,0 +1,32 @@ +from pathlib import Path + +from shotbible.check import check_bible +from shotbible.store import save + +from conftest import make_sample_bible, write_dummy_png + + +def test_check_rejects_relative_path_escape(tmp_path: Path) -> None: + root = tmp_path / "escape" + root.mkdir() + outside = tmp_path / "outside.png" + write_dummy_png(outside) + bible = make_sample_bible(with_ref=False) + bible.characters["mei"].refs = ["../outside.png"] + save(root, bible) + + issues = check_bible(root, bible) + assert any(getattr(issue, "code", None) == "MISSING_REF" for issue in issues) + + +def test_check_rejects_absolute_path_outside_project(tmp_path: Path) -> None: + root = tmp_path / "abs" + root.mkdir() + outside = tmp_path / "outside.png" + write_dummy_png(outside) + bible = make_sample_bible(with_ref=False) + bible.characters["mei"].refs = [str(outside.resolve())] + save(root, bible) + + issues = check_bible(root, bible) + assert any(getattr(issue, "code", None) == "MISSING_REF" for issue in issues)