fix: reject absolute urls in the content resolver, register the mutants

Review CODE-REVIEW-225-r1.

M1: urlsplit(url).path was trusted even when the url carried a scheme or an
authority, so "https://evil.example/houseplan_files/files/m1/doc.pdf"
resolved onto a local file while _looks_internal kept calling it external —
the mirror image of the inconsistency this resolver exists to prevent. Only a
same-document reference is resolved by its path now.

M2: the three mutants the spec described are registered in
scripts/mutation-gate.mjs instead of living as a one-off manual run. The
traversal entry drops both structural checks at once on purpose: taken one at
a time the defence is layered (sanitize_marker_id turns ".." into "misc") and
the mutant would be equivalent — established by running it.

Issue: #225
User-Visible: no
This commit is contained in:
Codex
2026-08-20 22:00:22 +03:00
parent cb1e4cea64
commit 2935c293e1
3 changed files with 71 additions and 1 deletions
+10 -1
View File
@@ -357,7 +357,16 @@ def _internal_path(root: Path, url: str) -> tuple[str, Path] | None:
# every backup holding one refused to import (issue #225). Path segments
# keep doing the guarding: dropping the query cannot widen what a segment
# is allowed to be.
url = urlsplit(url).path
#
# Only a same-document reference may be trusted this way: with a scheme or
# an authority the path belongs to another host, and taking it would let
# "https://evil.example/houseplan_files/files/m1/doc.pdf" resolve onto a
# local file (review CODE-REVIEW-225-r1, M1). Such a url stays external,
# which is also what _looks_internal says about it.
parsed = urlsplit(url)
if parsed.scheme or parsed.netloc:
return None
url = parsed.path
content_plan = CONTENT_URL + "/plans/_/"
if url.startswith(content_plan) or url.startswith(PLANS_URL + "/"):
prefix = content_plan if url.startswith(content_plan) else PLANS_URL + "/"