fix(git-wrappers): #865 round-7 addendum — conservative YAML recognizer + review/comment hardening
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful
Fold the remaining round-7 audit items into the tea-CLI comment-invocation fix. Fallback token parser (detect-platform.sh, test-gitea-login-resolution.sh): Replace the line-by-line scalar fallback with a strict CONSERVATIVE block-YAML recognizer that reconstructs the same object PyYAML would or fails closed the instant it meets anything outside the tea-config subset. Closes 5 structural fail-open classes the old parser missed (nested-shadow logins, block-scalar shadow, duplicate root key / login name / token field, malformed-after-valid, extra-document / end-marker). Validated by a 360k-check differential fuzz vs real PyYAML (0 fail-open) plus explicit forced-PyYAML-absence fixtures. ITEM 1 (pr-review.sh) current-head TOCTOU: after the exact review-id read-back succeeds, re-read the live PR head and fail closed if it advanced past the submitted commit_id, so a review is never reported as covering a superseded tip. ITEM 2 (pr-review.sh comment action): require the returned resource be a pull_request (populated pull_request_url); reject a bare issue_url so a plain issue #N cannot masquerade as a verified PR comment. issue-comment.sh keeps its broader issue-or-PR acceptance. ITEM 3a (both wrappers): move the Authorization bearer OUT of curl argv into a private mode-0600 curl --config file (gitea_write_auth_config), removed on every exit path, so the token never appears in the process table. ITEM 3b (pr-review.sh): bind the review body with presence + string-type + exact equality instead of `(body or "")`, so a non-empty submitted body persisted as null/missing fails closed. Tests: add race, plain-issue, argv-capture (no token printed), and null-body fixtures; broaden temp-leak checks to the auth-config files. Full gate set green (bash -n, shellcheck -x -S warning, prettier, all 3 REST/resolution suites with PyYAML and forced-absent, cold TURBO_FORCE turbo 14/14). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -52,6 +52,8 @@ BIN_DIR="$WORK_DIR/bin"
|
||||
XDG_DIR="$WORK_DIR/xdg"
|
||||
TEA_LOG="$WORK_DIR/tea.log"
|
||||
CURL_LOG="$WORK_DIR/curl.log"
|
||||
# Full curl argv per invocation — proves the bearer token never rides in argv.
|
||||
CURL_ARGV_LOG="$WORK_DIR/curl-argv.log"
|
||||
AUTH_LOG="$WORK_DIR/auth.log"
|
||||
OUTPUT_FILE="$WORK_DIR/output.log"
|
||||
CREDENTIALS_FILE="$WORK_DIR/credentials.json"
|
||||
@@ -155,17 +157,24 @@ cat > "$BIN_DIR/curl" <<'SH'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Record the FULL argv exactly as spawned, before consumption. The bearer token
|
||||
# must NOT appear here — it is delivered via a curl --config file (#865 ITEM 3a),
|
||||
# so only the config file PATH may show up.
|
||||
printf '%s\n' "$*" >> "$ISSUE_COMMENT_CURL_ARGV_LOG"
|
||||
|
||||
output_file=""
|
||||
method="GET"
|
||||
url=""
|
||||
data=""
|
||||
auth_token=""
|
||||
config_file=""
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-o) output_file="$2"; shift 2 ;;
|
||||
-H)
|
||||
[[ "$2" == Authorization:* ]] && auth_token="${2##* }"
|
||||
shift 2 ;;
|
||||
-K|--config) config_file="$2"; shift 2 ;;
|
||||
-w) shift 2 ;;
|
||||
-X) method="$2"; shift 2 ;;
|
||||
-d|--data) data="$2"; shift 2 ;;
|
||||
@@ -175,6 +184,17 @@ while [[ $# -gt 0 ]]; do
|
||||
esac
|
||||
done
|
||||
|
||||
# Resolve the bearer token from the curl --config file (its real, secure source);
|
||||
# fall back to an -H header only for defense in depth. The config line is
|
||||
# `header = "Authorization: token <value>"`.
|
||||
if [[ -z "$auth_token" && -n "$config_file" && -f "$config_file" ]]; then
|
||||
config_hdr="$(grep -i 'Authorization' "$config_file" 2>/dev/null || true)"
|
||||
if [[ "$config_hdr" == *"token "* ]]; then
|
||||
auth_token="${config_hdr##*token }"
|
||||
auth_token="${auth_token%\"}"
|
||||
fi
|
||||
fi
|
||||
|
||||
path="${url%%\?*}"
|
||||
query="${url#*\?}"
|
||||
[[ "$query" == "$url" ]] && query=""
|
||||
@@ -338,6 +358,7 @@ run_comment() {
|
||||
shift
|
||||
: > "$TEA_LOG"
|
||||
: > "$CURL_LOG"
|
||||
: > "$CURL_ARGV_LOG"
|
||||
: > "$AUTH_LOG"
|
||||
: > "$OUTPUT_FILE"
|
||||
seed_state "$mode"
|
||||
@@ -349,6 +370,7 @@ run_comment() {
|
||||
MOSAIC_CREDENTIALS_FILE="$CREDENTIALS_FILE" \
|
||||
ISSUE_COMMENT_TEA_LOG="$TEA_LOG" \
|
||||
ISSUE_COMMENT_CURL_LOG="$CURL_LOG" \
|
||||
ISSUE_COMMENT_CURL_ARGV_LOG="$CURL_ARGV_LOG" \
|
||||
ISSUE_COMMENT_AUTH_LOG="$AUTH_LOG" \
|
||||
ISSUE_COMMENT_STATE="$STATE_FILE" \
|
||||
ISSUE_COMMENT_TEST_MODE="$mode" \
|
||||
@@ -371,7 +393,9 @@ run_comment() {
|
||||
# clobbered/leaked RETURN trap is caught on every exit route.
|
||||
assert_no_temp_leak() {
|
||||
local context="$1" leaked
|
||||
leaked=$(find "$TMP_SCRATCH" -type f -name 'mosaic-issue-comment-*' 2>/dev/null || true)
|
||||
# Includes the curl auth-config files (mosaic-gitea-auth-*), which carry the
|
||||
# bearer token and must be unlinked on every exit path.
|
||||
leaked=$(find "$TMP_SCRATCH" -type f \( -name 'mosaic-issue-comment-*' -o -name 'mosaic-gitea-auth-*' \) 2>/dev/null || true)
|
||||
if [[ -n "$leaked" ]]; then
|
||||
echo "FAIL: issue-comment temp files leaked ($context):" >&2
|
||||
printf '%s\n' "$leaked" >&2
|
||||
@@ -379,6 +403,21 @@ assert_no_temp_leak() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Assert the presented bearer token NEVER appeared in curl's argv (it must travel
|
||||
# via a curl --config file), and that --config auth was actually used. On the
|
||||
# expected path grep matches nothing, so no token value is ever printed.
|
||||
assert_token_not_in_argv() {
|
||||
local context="$1"
|
||||
if grep -qF -e "$DEFAULT_TOKEN" -e "$OVERRIDE_TOKEN" -e "$CROSS_HOST_TOKEN" "$CURL_ARGV_LOG"; then
|
||||
echo "FAIL: a Gitea bearer token leaked into curl argv ($context)" >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! grep -q -- '--config' "$CURL_ARGV_LOG"; then
|
||||
echo "FAIL: curl was not invoked with --config file auth ($context)" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Case 1: a genuine REST create (id 51) is verified end to end via its exact
|
||||
# provider-returned id — no list enumeration is involved.
|
||||
run_comment fresh-success
|
||||
@@ -404,6 +443,9 @@ grep -q "^POST $API_BASE/issues/7/comments $ACTING_LOGIN$" "$AUTH_LOG"
|
||||
grep -q "^GET $API_BASE/issues/comments/51 $ACTING_LOGIN$" "$AUTH_LOG"
|
||||
# Success path leaves no scratch temp files behind.
|
||||
assert_no_temp_leak "fresh-success"
|
||||
# ITEM 3a: the token drove the write/read-back chain but never appeared in curl
|
||||
# argv — it was passed via a curl --config file.
|
||||
assert_token_not_in_argv "fresh-success default-token"
|
||||
|
||||
# Case 2: a no-op write with a concurrent SAME-IDENTITY, same-body comment
|
||||
# already present must FAIL CLOSED — the closed concurrency window.
|
||||
|
||||
Reference in New Issue
Block a user