From b0b2c20da09e30c8064fe6d58d5519eea780a711 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Thu, 18 Jun 2026 13:51:18 -0500 Subject: [PATCH] fix(framework/tools): eval injection, broken JSON, tmpfile leak (#548) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit F-01 (HIGH): issue-edit.sh and issue-assign.sh used string interpolation + eval to build CLI commands. Replace all eval sites with Bash arrays so user-supplied values (title, body, labels) are never shell-expanded. For the Gitea path, replace get_gitea_repo_args() (which emits %q-escaped strings designed for eval) with get_repo_slug() + get_gitea_login() so repo/login are passed as properly-quoted array elements. F-07 (MED): milestone-create.sh built the GitHub API JSON payload by string interpolation — a title containing " or $ broke the JSON. Rebuild with jq -n --arg so all values are safely serialised. Optional description key is omitted when empty, preserving existing behaviour. F-13 (LOW): pr-metadata.sh created a mktemp tmpfile inside curl_gitea_pull() but only removed it in success paths. Add trap 'rm -f "$body_file"' EXIT immediately after mktemp so early-exit paths (set -e, SIGINT) also clean up. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01Kt2D8TsnDwhtzEAPijsNmR --- .../framework/tools/git/issue-assign.sh | 21 +++++++----- .../mosaic/framework/tools/git/issue-edit.sh | 32 +++++++++++-------- .../framework/tools/git/milestone-create.sh | 13 +++++--- .../mosaic/framework/tools/git/pr-metadata.sh | 2 ++ 4 files changed, 42 insertions(+), 26 deletions(-) diff --git a/packages/mosaic/framework/tools/git/issue-assign.sh b/packages/mosaic/framework/tools/git/issue-assign.sh index f3a15cb..6151b0e 100755 --- a/packages/mosaic/framework/tools/git/issue-assign.sh +++ b/packages/mosaic/framework/tools/git/issue-assign.sh @@ -98,27 +98,32 @@ case "$PLATFORM" in ;; gitea) # tea issue edit syntax - REPO_ARGS=$(get_gitea_repo_args) || { - echo "Error: Could not resolve Gitea repo/login args for remote host" >&2 + REPO_SLUG=$(get_repo_slug) || { + echo "Error: Could not resolve Gitea repo slug from remote" >&2 exit 1 } - CMD="tea issue edit $ISSUE $REPO_ARGS" + REPO_LOGIN=$(get_gitea_login) || { + echo "Error: Could not resolve Gitea login for remote host" >&2 + exit 1 + } + REPO_ARGS=(--repo "$REPO_SLUG" --login "$REPO_LOGIN") + CMD=(tea issue edit "$ISSUE" "${REPO_ARGS[@]}") NEEDS_EDIT=false if [[ -n "$ASSIGNEE" ]]; then # tea uses --assignees flag - CMD="$CMD --assignees \"$ASSIGNEE\"" + CMD+=(--assignees "$ASSIGNEE") NEEDS_EDIT=true fi if [[ -n "$LABELS" ]]; then # tea uses --labels flag (replaces existing) - CMD="$CMD --labels \"$LABELS\"" + CMD+=(--labels "$LABELS") NEEDS_EDIT=true fi if [[ -n "$MILESTONE" ]]; then - MILESTONE_ID=$(tea milestones list $REPO_ARGS 2>/dev/null | grep -E "^\s*[0-9]+" | grep "$MILESTONE" | awk '{print $1}' | head -1) + MILESTONE_ID=$(tea milestones list "${REPO_ARGS[@]}" 2>/dev/null | grep -E "^\s*[0-9]+" | grep "$MILESTONE" | awk '{print $1}' | head -1) if [[ -n "$MILESTONE_ID" ]]; then - CMD="$CMD --milestone $MILESTONE_ID" + CMD+=(--milestone "$MILESTONE_ID") NEEDS_EDIT=true else echo "Warning: Could not find milestone '$MILESTONE'" >&2 @@ -126,7 +131,7 @@ case "$PLATFORM" in fi if [[ "$NEEDS_EDIT" == true ]]; then - eval "$CMD" + "${CMD[@]}" echo "Issue #$ISSUE updated successfully" else echo "No changes specified" diff --git a/packages/mosaic/framework/tools/git/issue-edit.sh b/packages/mosaic/framework/tools/git/issue-edit.sh index 0ffcc0b..865af4f 100755 --- a/packages/mosaic/framework/tools/git/issue-edit.sh +++ b/packages/mosaic/framework/tools/git/issue-edit.sh @@ -63,24 +63,28 @@ fi detect_platform >/dev/null if [[ "$PLATFORM" == "github" ]]; then - CMD="gh issue edit $ISSUE_NUMBER" - [[ -n "$TITLE" ]] && CMD="$CMD --title \"$TITLE\"" - [[ -n "$BODY" ]] && CMD="$CMD --body \"$BODY\"" - [[ -n "$LABELS" ]] && CMD="$CMD --add-label \"$LABELS\"" - [[ -n "$MILESTONE" ]] && CMD="$CMD --milestone \"$MILESTONE\"" - eval $CMD + CMD=(gh issue edit "$ISSUE_NUMBER") + [[ -n "$TITLE" ]] && CMD+=(--title "$TITLE") + [[ -n "$BODY" ]] && CMD+=(--body "$BODY") + [[ -n "$LABELS" ]] && CMD+=(--add-label "$LABELS") + [[ -n "$MILESTONE" ]] && CMD+=(--milestone "$MILESTONE") + "${CMD[@]}" echo "Updated GitHub issue #$ISSUE_NUMBER" elif [[ "$PLATFORM" == "gitea" ]]; then - REPO_ARGS=$(get_gitea_repo_args) || { - echo "Error: Could not resolve Gitea repo/login args for remote host" >&2 + REPO_SLUG=$(get_repo_slug) || { + echo "Error: Could not resolve Gitea repo slug from remote" >&2 exit 1 } - CMD="tea issue edit $ISSUE_NUMBER $REPO_ARGS" - [[ -n "$TITLE" ]] && CMD="$CMD --title \"$TITLE\"" - [[ -n "$BODY" ]] && CMD="$CMD --description \"$BODY\"" - [[ -n "$LABELS" ]] && CMD="$CMD --add-labels \"$LABELS\"" - [[ -n "$MILESTONE" ]] && CMD="$CMD --milestone \"$MILESTONE\"" - eval $CMD + REPO_LOGIN=$(get_gitea_login) || { + echo "Error: Could not resolve Gitea login for remote host" >&2 + exit 1 + } + CMD=(tea issue edit "$ISSUE_NUMBER" --repo "$REPO_SLUG" --login "$REPO_LOGIN") + [[ -n "$TITLE" ]] && CMD+=(--title "$TITLE") + [[ -n "$BODY" ]] && CMD+=(--description "$BODY") + [[ -n "$LABELS" ]] && CMD+=(--add-labels "$LABELS") + [[ -n "$MILESTONE" ]] && CMD+=(--milestone "$MILESTONE") + "${CMD[@]}" echo "Updated Gitea issue #$ISSUE_NUMBER" else echo "Error: Unknown platform" diff --git a/packages/mosaic/framework/tools/git/milestone-create.sh b/packages/mosaic/framework/tools/git/milestone-create.sh index c5ffb52..8f5c371 100755 --- a/packages/mosaic/framework/tools/git/milestone-create.sh +++ b/packages/mosaic/framework/tools/git/milestone-create.sh @@ -99,10 +99,15 @@ fi case "$PLATFORM" in github) # GitHub uses the API for milestone creation - JSON_PAYLOAD="{\"title\":\"$TITLE\"" - [[ -n "$DESCRIPTION" ]] && JSON_PAYLOAD="$JSON_PAYLOAD,\"description\":\"$DESCRIPTION\"" - [[ -n "$DUE_DATE" ]] && JSON_PAYLOAD="$JSON_PAYLOAD,\"due_on\":\"${DUE_DATE}T00:00:00Z\"" - JSON_PAYLOAD="$JSON_PAYLOAD}" + # Use jq to safely construct JSON so titles/descriptions containing + # quotes or special characters do not corrupt the payload (F-07). + JSON_PAYLOAD=$(jq -n \ + --arg t "$TITLE" \ + --arg d "$DESCRIPTION" \ + --arg due "${DUE_DATE}" \ + '{"title": $t} + + (if $d != "" then {"description": $d} else {} end) + + (if $due != "" then {"due_on": ($due + "T00:00:00Z")} else {} end)') gh api repos/:owner/:repo/milestones --method POST --input - <<< "$JSON_PAYLOAD" echo "Milestone '$TITLE' created successfully" diff --git a/packages/mosaic/framework/tools/git/pr-metadata.sh b/packages/mosaic/framework/tools/git/pr-metadata.sh index b6373bf..7e8d1fc 100755 --- a/packages/mosaic/framework/tools/git/pr-metadata.sh +++ b/packages/mosaic/framework/tools/git/pr-metadata.sh @@ -56,6 +56,8 @@ curl_gitea_pull() { local api_url="$1" local token basic_auth raw_code body_file http_code body_file=$(mktemp) + # Ensure the tmpfile is removed even on early exit (set -e, SIGINT, etc.) + trap 'rm -f "$body_file"' EXIT token=$(get_gitea_token "$HOST" || true) if [[ -n "$token" ]]; then