fix(git-tools): scope-aware YAML fallback, port-bound creds, origin-pinned URL + review-body verification (#865)
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful
Round-6 remediation for PR #866 addressing four cross-host exact-diff audit blockers (REQUEST_CHANGES governs): Blocker 1 (detect-platform.sh get_gitea_token_for_login line parser): the PyYAML-absence fallback attributed any `key: value` at any depth to the current login, so a token from a nested sub-map or a mis-indented line could be selected where PyYAML fails closed, and inline comments were not stripped. The fallback is now scope-aware — a field attaches only at the entry's own direct-field indentation, only list items at the login list's own dash indent open an entry — and _strip_scalar strips a trailing inline comment like PyYAML. It is therefore only ever MORE conservative than PyYAML, never less. Blocker 2 (detect-platform.sh host bind): credential binding compared parsed.hostname only, dropping the port, so a :9443 login satisfied a portless host and a matching :8443 login was rejected. Binding now normalizes scheme + host + effective port (scheme default applied symmetrically) exactly like gitea_url_matches_host. Blocker 3 (issue-comment.sh + pr-review.sh read-back URL check): verification used path.endswith, accepting a look-alike host or a decoy path prefix. It now pins the returned issue_url/pull_request_url ORIGIN (scheme+host+effective-port) and FULL path (deployment prefix + exact owner/repo + kind + number). A new GITEA_WEB_BASE is exported from gitea_resolve_api_for_login for this. Blocker 4 (pr-review.sh gitea_submit_review_verified): the submitted review body was not verified, so a finalized/reused pending review id carrying foreign Content passed. The persisted body is now bound to the exact submitted body. Tests: added forced-PyYAML-absence parser-equivalence fixtures (nested sub-map, sibling, mis-indent, inline comment, tab-indent fail-closed, port match/mismatch) to test-gitea-login-resolution.sh; URL-forgery fail-closed cases (wrong-host/owner/repo + prefix injection) to both write suites; and a reused-review-id body-mismatch case to the pr-review suite. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -232,13 +232,25 @@ if mode == "no-op-concurrent":
|
||||
|
||||
author = foreign if mode == "author-mismatch" else acting
|
||||
new_id = (max((c["id"] for c in comments), default=0)) + 1
|
||||
# REAL Gitea comment shape: issue_url is the WEB (html) path, not an API path,
|
||||
# and a plain issue comment leaves pull_request_url empty. The URL-injection
|
||||
# modes persist a record whose id/author/body are all correct but whose
|
||||
# issue_url is forged, so ONLY the origin+path verification can catch them.
|
||||
issue_url = f"https://git.mosaicstack.dev/{repo}/issues/7"
|
||||
if mode == "url-wrong-host":
|
||||
issue_url = f"https://evil.example/{repo}/issues/7"
|
||||
elif mode == "url-wrong-owner":
|
||||
issue_url = "https://git.mosaicstack.dev/attacker/stack/issues/7"
|
||||
elif mode == "url-wrong-repo":
|
||||
issue_url = "https://git.mosaicstack.dev/mosaicstack/other/issues/7"
|
||||
elif mode == "url-suffix-injection":
|
||||
# Prefix-injected: a bare endswith("/<slug>/issues/7") test would ACCEPT this.
|
||||
issue_url = f"https://git.mosaicstack.dev/deceptive/{repo}/issues/7"
|
||||
record = {
|
||||
"id": new_id,
|
||||
"body": body,
|
||||
"user": {"login": author},
|
||||
# REAL Gitea comment shape: issue_url is the WEB (html) path, not an API
|
||||
# path, and a plain issue comment leaves pull_request_url empty.
|
||||
"issue_url": f"https://git.mosaicstack.dev/{repo}/issues/7",
|
||||
"issue_url": issue_url,
|
||||
"pull_request_url": "",
|
||||
}
|
||||
comments.append(record)
|
||||
@@ -491,4 +503,27 @@ if grep -q " $ACTING_LOGIN\$" "$AUTH_LOG"; then
|
||||
fi
|
||||
assert_no_temp_leak "cross-host"
|
||||
|
||||
# Cases 7-10 (#865 Blocker 3): the created record's id/author/body are all
|
||||
# correct, but its provider-returned issue_url is forged. Verification pins the
|
||||
# URL's ORIGIN (scheme+host+effective-port) and its FULL path (deployment prefix
|
||||
# + exact owner/repo + kind + number), so each forgery must FAIL CLOSED. A bare
|
||||
# endswith/suffix test would wrongly accept the look-alike-host and
|
||||
# prefix-injection variants.
|
||||
for bad_mode in url-wrong-host url-wrong-owner url-wrong-repo url-suffix-injection; do
|
||||
if run_comment "$bad_mode"; then
|
||||
echo "FAIL: forged comment URL ($bad_mode) was accepted" >&2
|
||||
cat "$OUTPUT_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
if grep -q 'Added and verified comment' "$OUTPUT_FILE"; then
|
||||
echo "FAIL: forged comment URL ($bad_mode) passed verification" >&2
|
||||
exit 1
|
||||
fi
|
||||
assert_no_temp_leak "$bad_mode"
|
||||
done
|
||||
|
||||
# Sanity: the exact same verification path still ACCEPTS a legitimate web-shaped
|
||||
# issue_url (already exercised by Case 1's fresh-success), so the tightened check
|
||||
# is not rejecting genuine writes.
|
||||
|
||||
echo "issue-comment.sh REST create + exact-id read-back regression passed"
|
||||
|
||||
Reference in New Issue
Block a user