From 2b6a4f099319e24ce6b10c450f70f62a3b7ecdd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BC=98=E9=9B=85=E3=81=AE=E5=92=B8=E9=B1=BC?= <3129538298@qq.com> Date: Wed, 26 Aug 2026 16:55:12 +0800 Subject: [PATCH 1/3] Do not follow ../ refs out of the project during check. --- src/shotbible/check.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/shotbible/check.py b/src/shotbible/check.py index 76605aa..bfbb20b 100644 --- a/src/shotbible/check.py +++ b/src/shotbible/check.py @@ -217,7 +217,13 @@ def _ref_exists(root: Path, ref: str) -> bool: path = Path(ref) if path.is_absolute(): return path.exists() - return (root / ref).exists() + root = root.resolve() + candidate = root / ref + try: + candidate.resolve().relative_to(root) + except ValueError: + return False + return candidate.exists() def _normalize_look(look: str) -> str: From a6e071bc34e0f23ee3399fe7f50c2980931d146e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BC=98=E9=9B=85=E3=81=AE=E5=92=B8=E9=B1=BC?= <3129538298@qq.com> Date: Wed, 26 Aug 2026 16:55:35 +0800 Subject: [PATCH 2/3] Test that check treats ../ refs as missing. --- tests/test_ref_escape.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 tests/test_ref_escape.py diff --git a/tests/test_ref_escape.py b/tests/test_ref_escape.py new file mode 100644 index 0000000..5237a4b --- /dev/null +++ b/tests/test_ref_escape.py @@ -0,0 +1,19 @@ +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) From 3312accf7c6718c9380623e9d6ae7b7d6b9bc9a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BC=98=E9=9B=85=E3=81=AE=E5=92=B8=E9=B1=BC?= <3129538298@qq.com> Date: Wed, 26 Aug 2026 17:32:33 +0800 Subject: [PATCH 3/3] Treat absolute refs outside the project as missing. --- src/shotbible/check.py | 11 +++++------ tests/test_ref_escape.py | 13 +++++++++++++ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/shotbible/check.py b/src/shotbible/check.py index bfbb20b..16163e4 100644 --- a/src/shotbible/check.py +++ b/src/shotbible/check.py @@ -214,14 +214,13 @@ def _listed_refs(bible: Bible) -> set[str]: def _ref_exists(root: Path, ref: str) -> bool: if not str(ref).strip(): return False - path = Path(ref) - if path.is_absolute(): - return path.exists() root = root.resolve() - candidate = root / ref + path = Path(ref) + candidate = path if path.is_absolute() else root / ref try: - candidate.resolve().relative_to(root) - except ValueError: + resolved = candidate.resolve() + resolved.relative_to(root) + except (ValueError, OSError): return False return candidate.exists() diff --git a/tests/test_ref_escape.py b/tests/test_ref_escape.py index 5237a4b..ac02760 100644 --- a/tests/test_ref_escape.py +++ b/tests/test_ref_escape.py @@ -17,3 +17,16 @@ def test_check_rejects_relative_path_escape(tmp_path: Path) -> None: 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)