From 8504216964c1ee667ffba2c3e695700a26085051 Mon Sep 17 00:00:00 2001 From: "jason.woltje" Date: Thu, 23 Jul 2026 17:53:26 +0000 Subject: [PATCH] fix(pr-review): case-insensitive _belongs slug compare (#875) Closes #875 Mos (id-11) Gate-16 merge: independent APPROVE @9d8d58ae, author id2 != approver id11, clean mosaic-coder commit-author, CI green wp1982. Co-authored-by: jason.woltje Co-committed-by: jason.woltje --- .../mosaic/framework/tools/git/pr-review.sh | 10 +++++++++- .../tools/git/test-pr-review-gitea-comment.sh | 20 +++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/packages/mosaic/framework/tools/git/pr-review.sh b/packages/mosaic/framework/tools/git/pr-review.sh index 54dfde97..584b05e7 100755 --- a/packages/mosaic/framework/tools/git/pr-review.sh +++ b/packages/mosaic/framework/tools/git/pr-review.sh @@ -203,7 +203,15 @@ try: if not url: return False origin, path = _origin_and_path(url) - return origin == base_origin and path == expected_path + # Repo owner/repo slugs are case-insensitive (Gitea canonicalizes the + # pull_request_url slug to lowercase on return), while EXPECTED_REPO_SLUG + # is taken verbatim from GITEA_API_BASE and may be mixed-case. The + # remainder of the path (".../pulls/") is numeric, so lowercasing + # the whole path for this comparison only relaxes case, not identity: the + # origin tuple (scheme+host+port) above still pins the provider host, and + # the path is still compared in FULL (no endswith/suffix match), so the + # look-alike-host and same-host decoy-prefix protections are unchanged. + return origin == base_origin and path.lower() == expected_path.lower() if comment.get("id") != expected_id: raise ValueError("read-back id does not match the created id") diff --git a/packages/mosaic/framework/tools/git/test-pr-review-gitea-comment.sh b/packages/mosaic/framework/tools/git/test-pr-review-gitea-comment.sh index 90f57c85..cf1de2fb 100644 --- a/packages/mosaic/framework/tools/git/test-pr-review-gitea-comment.sh +++ b/packages/mosaic/framework/tools/git/test-pr-review-gitea-comment.sh @@ -406,6 +406,13 @@ elif mode == "comment-url-wrong-repo": elif mode == "comment-url-suffix-injection": # Prefix-injected: a bare endswith("//pulls/123") test would ACCEPT it. pr_url = f"{_origin}/deceptive{_slug}/pulls/123" +elif mode == "comment-mixed-case-slug": + # #875: EXPECTED_REPO_SLUG is taken verbatim from GITEA_API_BASE and can be + # mixed-case (e.g. "USC/uconnect"), but Gitea canonicalizes the returned + # pull_request_url's owner/repo segment to LOWERCASE. Model that here by + # lowercasing only the slug path, independent of the (possibly mixed-case) + # web_base the wrapper was configured with. + pr_url = f"{_origin}{_slug.lower()}/pulls/123" record = { "id": 456, "body": body, @@ -868,6 +875,19 @@ for bad_mode in comment-url-wrong-host comment-url-wrong-owner comment-url-wrong assert_no_temp_leak "$bad_mode" done +# Case 15b (#875): a MIXED-CASE repo slug (as embedded verbatim in +# GITEA_API_BASE, e.g. "USC/uconnect") must still verify when Gitea returns the +# comment's pull_request_url with its owner/repo segment canonicalized to +# LOWERCASE ("usc/uconnect"). This is a legitimate, unforged provider response — +# not a spoof — so `_belongs` must accept it (case-insensitive slug compare) +# while still requiring the origin (scheme+host+port) and the rest of the path +# to match in full. Pre-#875-fix this fails closed on a real success +# (false-negative); post-fix it verifies. +run_review comment-mixed-case-slug comment durable-body https://git.mosaicstack.dev \ + https://git.mosaicstack.dev/USC/uconnect.git USC/uconnect +grep -q 'Added and verified comment on Gitea PR #123' "$OUTPUT_FILE" +assert_no_temp_leak "comment-mixed-case-slug" + # Case 16 (#865 ITEM 1, current-head TOCTOU): the PR head advances between the # pre-submit head read (which pins the review) and the post-verify re-read. The # review is genuinely created and verified as pinned to the OLD head, but the