fix(retrieval): fail-closed on uncanonicalisable paths (embedded null byte)

is_within_dir called os.path.realpath OUTSIDE its try block, so a path with
an embedded null byte (which makes realpath raise ValueError, not OSError)
leaked that ValueError to callers. Via safe_resolve -> okf._load_file (which
catches only PathSecurityError) it propagated uncaught out of
navigate_bundle, breaking the OKF SPEC §4 guarantee (okf.py:8-9) that a
broken cross-link is silently skipped, never raised: an index link like
`](a\x00b.md)` has no `/`, slips past the same-dir pre-filter, and reached
path resolution.

Move both realpath calls inside the existing try so an uncanonicalisable
path is treated as not-within (fail-closed): safe_resolve raises
PathSecurityError -> _load_file returns None -> the link is skipped. Fixes
the class for both callers of the seam (navigate_bundle + retrieve).

Tests (load-bearing — RED before, GREEN after):
- test_navigate_skips_null_byte_link: the reported regression.
- test_null_byte_path_rejected: the seam directly (is_within_dir/safe_resolve).
- test_navigate_skips_bundle_escaping_symlink_link: closes the previously
  untested path-safety-at-link-resolution branch of navigate_bundle.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVHLJuwBzp7MXkrUXJYARS
This commit is contained in:
Kjell Tore Guttormsen 2026-07-21 08:00:11 +02:00
commit 3b0ee792bd
3 changed files with 50 additions and 4 deletions

View file

@ -84,3 +84,12 @@ def test_prefix_collision_sibling_rejected(tmp_path) -> None:
intruder = sibling / "secret.txt"
intruder.write_text("x", encoding="utf-8")
assert is_within_dir(str(intruder), str(root)) is False
def test_null_byte_path_rejected(docs) -> None:
# A path carrying an embedded null byte cannot be canonicalised (``os.path.realpath`` raises
# ``ValueError``); the seam MUST treat it as not-within / escaping, fail-closed — never leak the
# raw ``ValueError`` to callers.
assert is_within_dir("a\x00b.txt", str(docs)) is False
with pytest.raises(PathSecurityError):
safe_resolve(str(docs), "a\x00b.txt")