Skip to content

fix(pre-code): harden GHA workflow command injection surface - #576

Closed
ralphbean wants to merge 8 commits into
mainfrom
fix/pre-code-gha-injection-hardening
Closed

fix(pre-code): harden GHA workflow command injection surface#576
ralphbean wants to merge 8 commits into
mainfrom
fix/pre-code-gha-injection-hardening

Conversation

@ralphbean

@ralphbean ralphbean commented Jul 30, 2026

Copy link
Copy Markdown
Member

Summary

  • Move ::notice:: for GITHUB_ISSUE_URL after input validation so unvalidated values never reach GHA workflow command parsing
  • Replace raw COMMENT_BODY echo with body length to avoid user-controlled content hitting stdout where GHA scans for :: commands
  • Route validation-failure ::error:: lines and the CODE_FORCE force-override debug log through gha_echo, which sanitizes :: and CR/LF sequences so already-known-invalid or attacker-controlled input can't inject a second workflow command
  • Add defence-in-depth sanitization of gha_echo's own level parameter
  • Extract the sanitize/echo helpers into their own scripts/lib/gha-log-sanitize.lib.sh so pre-code.src.sh doesn't have to source the full post-failure-report.lib.sh (no gh calls, no comment-posting logic) just to reuse them

Split out from #567 per review feedback — these security-hardening changes are unrelated to the TRIAGE_AUTO_CODE feature.

The same injection class exists in pre-review.sh and pre-fix.sh; tracked separately in #598 since it's outside this PR's scope.

Test plan

  • make test passes
  • shellcheck clean on scripts/pre-code.src.sh
  • Verify bundled pre-code.sh matches source

🤖 Generated with Claude Code

ralphbean and others added 2 commits July 30, 2026 16:46
Move the ::notice:: for GITHUB_ISSUE_URL after input validation so
unvalidated values never reach GHA workflow command parsing. Replace
raw COMMENT_BODY echo with body length to avoid user-controlled content
hitting stdout where GHA scans for :: commands.

Signed-off-by: Ralph Bean <rbean@redhat.com>
Assisted-by: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Ralph Bean <rbean@redhat.com>
The source file pre-code.src.sh was modified but the bundled output
was not regenerated, causing the check-bundle CI step to fail.

Assisted-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Ralph Bean <rbean@redhat.com>
@ralphbean
ralphbean requested a review from a team as a code owner July 30, 2026 20:47
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Harden pre-code GitHub Actions logging against workflow command injection

🐞 Bug fix 🕐 20-40 Minutes

Grey Divider

AI Description

• Delay ::notice:: logging of GITHUB_ISSUE_URL until after input validation.
• Stop echoing user-controlled COMMENT_BODY; log only its length during force-override checks.
• Regenerate bundled scripts/pre-code.sh to match updated scripts/pre-code.src.sh.
Diagram

graph TD
  A{{"GitHub Actions"}} --> B["pre-code script"] --> C["Input validation"] --> D["::notice Code target"]
  B --> E["Force override check"] --> F["Log body length"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Escape/sanitize values before emitting any `::notice::`
  • ➕ Allows early logging while reducing workflow-command injection risk
  • ➕ Keeps current UX (notice output) unchanged in placement
  • ➖ Easy to get escaping wrong or miss edge cases (CR/LF, %, :: sequences)
  • ➖ Adds complexity compared to simply reordering and reducing log content
2. Avoid workflow commands entirely (use step summary or plain logs)
  • ➕ Eliminates the :: command-parsing surface in this script
  • ➕ Simplifies threat model for user-controlled text in logs
  • ➖ Loses GitHub-native notice annotation behavior unless replaced with another mechanism
  • ➖ May require workflow changes to surface the same information

Recommendation: The PR’s approach (move the ::notice:: emission until after validation and stop echoing user-controlled bodies) is the safest low-complexity mitigation and is appropriate for hardening. Alternatives like escaping/sanitizing can work but are more error-prone in shell and harder to audit over time.

Files changed (2) +8 / -6

Bug fix (2) +8 / -6
pre-code.shEmit '::notice::' only after validation; avoid logging comment body +4/-3

Emit '::notice::' only after validation; avoid logging comment body

• Moves the '::notice::' message that includes 'GITHUB_ISSUE_URL' to after input validation so unvalidated values do not reach GitHub Actions command parsing. Replaces logging of raw 'COMMENT_BODY' with logging only the body length to prevent user-controlled '::' sequences from being interpreted as workflow commands.

scripts/pre-code.sh

pre-code.src.shHarden source pre-code script against GHA log-command injection +4/-3

Harden source pre-code script against GHA log-command injection

• Reorders '::notice::' output for the code target URL to occur only after validation succeeds. Stops echoing the full 'COMMENT_BODY' during force override evaluation and logs only its length instead.

scripts/pre-code.src.sh

@fullsend-ai-review

fullsend-ai-review Bot commented Jul 30, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 8:49 PM UTC · Completed 9:02 PM UTC
Commit: 3bfce3f · View workflow run →

@qodo-code-review

qodo-code-review Bot commented Jul 30, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📜 Skill insights (1)

Grey Divider


Action required

1. Protected scripts/ files modified 📜 Skill insight § Compliance
Description
This PR modifies files under scripts/, which is a protected governance/infrastructure path and
must not be auto-approved. These changes require explicit human review and approval per the
protected-path policy.
Code

scripts/pre-code.src.sh[R59-60]

+echo "::notice::🔗 Code target: ${GITHUB_ISSUE_URL}"
+
Relevance

●●● Strong

Protected-path findings are enforced; schema blocks approvals when protected-path present (PR #303).

PR-#303

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The protected-path rule requires raising a finding whenever files under protected directories
(including scripts/) are modified. The diff shows changes in scripts/pre-code.src.sh (and the
generated scripts/pre-code.sh), which are within the protected scripts/ path.

scripts/pre-code.src.sh[59-60]
scripts/pre-code.sh[86-87]
Skill: pr-review


2. No tests for new stdout ✓ Resolved 📜 Skill insight ▣ Testability
Description
The PR changes pre-code runtime behavior (stdout output) but does not include corresponding test
updates to constrain the new security behavior. This can allow regressions where user-controlled
content re-enters GitHub Actions command parsing without detection.
Code

scripts/pre-code.src.sh[R84-85]

+_cb="${COMMENT_BODY:-}"
+echo "Evaluating force override: CODE_FORCE='${CODE_FORCE:-}' COMMENT_BODY_LENGTH='${#_cb}'"
Relevance

●● Moderate

Repo values script tests (PR #536), but no clear precedent requiring assertions for new stdout logs.

PR-#536

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The compliance rule requires test changes when production behavior changes. The diff introduces new
stdout behavior in scripts/pre-code.src.sh (printing GITHUB_ISSUE_URL as a workflow command
after validation and replacing the prior COMMENT_BODY echo with a length-only log), while the
existing test file focuses on PR-search/force-override behavior and does not add assertions for
these new stdout constraints.

scripts/pre-code.src.sh[59-60]
scripts/pre-code.src.sh[84-85]
scripts/pre-code-test.sh[305-399]
Skill: code-implementation

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`scripts/pre-code.src.sh` changed stdout behavior (moved `::notice::` emission and replaced raw `COMMENT_BODY` printing with length), but the PR does not update tests to assert these new constraints.

## Issue Context
There is an existing test harness (`scripts/pre-code-test.sh`) that validates pre-code behavior using a mock `gh`, but it currently does not assert the new security-hardening guarantees (e.g., that `COMMENT_BODY` content is not printed).

## Fix Focus Areas
- scripts/pre-code.src.sh[59-60]
- scripts/pre-code.src.sh[84-85]
- scripts/pre-code-test.sh[305-399]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context used
✅ Compliance rules (platform): 55 rules
✅ Skills: 4 invoked
  code-review
  code-implementation
  pr-review
  docs-review

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Comment thread scripts/pre-code.src.sh
Comment thread scripts/pre-code.src.sh Outdated
@fullsend-ai-review

fullsend-ai-review Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review

Findings

High

  • [protected-path] scripts/pre-code.src.sh, scripts/lib/gha-log-sanitize.lib.sh, scripts/lib/post-failure-report.lib.sh, scripts/bundle-sh.sh, scripts/pre-code-test.sh, scripts/pre-code.sh, scripts/post-code.sh, scripts/post-fix.sh — All changed files are under scripts/, a protected path requiring human approval. The PR has no linked issue providing authorization for modifying governance/infrastructure files. Human review and approval is required for all protected-path changes.
    Remediation: Obtain human approval for changes to protected paths. Optionally, create a linked issue documenting the security-hardening rationale.

Low

  • [edge-case] scripts/pre-code.src.sh:34 — When ISSUE_NUMBER contains :: (e.g., 12::34), _sanitize_workflow_value strips the :: and the error message displays got: '1234', which looks like a valid positive integer to an operator. The rejection is correct (the regex validates the unsanitized value), but the sanitized message loses diagnostic fidelity. This is an inherent security-over-debuggability tradeoff, covered by regression test.

Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR
Previous run

Review

Findings

High

  • [protected-path] scripts/pre-code.src.sh, scripts/pre-code.sh, scripts/pre-code-test.sh, scripts/lib/gha-log-sanitize.lib.sh, scripts/lib/post-failure-report.lib.sh, scripts/bundle-sh.sh, scripts/post-code.sh, scripts/post-fix.sh — All changed files are under scripts/, a protected path requiring human approval. The PR has no linked issue providing authorization for modifying governance/infrastructure files. Human review and approval is required for all protected-path changes.
    Remediation: Obtain human approval for changes to protected paths. Optionally, create a linked issue documenting the security-hardening rationale.

Low

  • [edge-case] scripts/pre-code.src.sh:32 — When ISSUE_NUMBER contains :: (e.g., 12::34), _sanitize_workflow_value strips the :: and the error message displays got: '1234', which looks like a valid positive integer to an operator. The rejection is correct (the regex validates the unsanitized value), but the sanitized message loses diagnostic fidelity. This is an inherent security-over-debuggability tradeoff, covered by regression test.

  • [workflow-command-injection] scripts/lib/gha-log-sanitize.lib.sh:61gha_echo sanitizes the message body but does not sanitize the level parameter ($1). In all current call sites, the level is a hardcoded string literal (error, warning, notice), so this is not exploitable today. Defense-in-depth suggestion: add validation or sanitization to the level parameter for future-proofing.

  • [workflow-command-injection] scripts/pre-code.src.sh — The ::notice::Found existing human PR line (not changed by this PR) interpolates values from GitHub API output into a raw ::notice:: without sanitization. Practically unexploitable (GitHub usernames exclude ::, PR numbers are integers), but inconsistent with the PR's hardening goal of routing all workflow commands through gha_echo.


Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR
Previous run (2)

Review

Findings

High

  • [protected-path] scripts/pre-code.sh, scripts/pre-code.src.sh, scripts/pre-code-test.sh — All changed files are under scripts/, a protected path requiring human approval. The PR has no linked issue providing authorization for modifying governance/infrastructure files. Human review and approval is required for all protected-path changes.
    Remediation: Obtain human approval for changes to protected paths. Optionally, create a linked issue documenting the security-hardening rationale.

Low

  • [edge-case] scripts/pre-code.src.sh:34 — When ISSUE_NUMBER contains :: (e.g., 12::34), _sanitize_workflow_value strips the :: and the error message displays got: '1234', which looks like a valid positive integer to an operator. The rejection is correct (the regex validates the unsanitized value), but the sanitized message loses diagnostic fidelity. This is an inherent security-over-debuggability tradeoff.

Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR
Previous run (3)

Review

Findings

High

  • [protected-path] scripts/pre-code.sh, scripts/pre-code.src.sh, scripts/pre-code-test.sh — All changed files are under scripts/, a protected path requiring human approval. The PR has no linked issue providing authorization for modifying governance/infrastructure files. Human review and approval is required for all protected-path changes.
    Remediation: Obtain human approval for changes to protected paths. Optionally, create a linked issue documenting the security-hardening rationale.

Low

  • [edge-case] scripts/pre-code.src.sh:34 — When ISSUE_NUMBER contains :: (e.g., 12::34), _sanitize_workflow_value strips the :: and the error message displays got: '1234', which looks like a valid positive integer to an operator. The rejection is correct (the regex validates the unsanitized value), but the sanitized message loses diagnostic fidelity. This is an inherent security-over-debuggability tradeoff.

  • [injection] scripts/pre-code.src.sh:92 — The force-override debug log still interpolates CODE_FORCE via plain echo. Although CODE_FORCE is typically a workflow_dispatch boolean input, any value containing a newline followed by a GHA workflow command would be emitted unsanitized to stdout. The COMMENT_BODY injection vector was correctly closed, but CODE_FORCE was left as-is. Risk is low since workflow_dispatch requires repository write access.
    Remediation: Use gha_echo for this log line, or sanitize CODE_FORCE similarly to COMMENT_BODY (e.g., log a boolean normalization).


Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR
Previous run (4)

Review

Findings

High

  • [protected-path] scripts/pre-code.sh, scripts/pre-code.src.sh — Both changed files are under scripts/, a protected path requiring human approval. The PR has no linked issue providing authorization for modifying governance/infrastructure files. Human review and approval is required for all protected-path changes.
    Remediation: Obtain human approval for changes to protected paths. Optionally, create a linked issue documenting the security-hardening rationale.

Low

  • [output-consistency] scripts/pre-code.src.sh:59 — The ::notice:: for GITHUB_ISSUE_URL is moved from before validation to after validation, which differs from the pattern in pre-triage.sh, pre-review.sh, and pre-prioritize.sh (but matches pre-retro.sh, which already validates before emitting its notice). The deviation is intentional security hardening to avoid passing unvalidated user input to GHA workflow command parsing. Consider applying the same validate-then-notice pattern to the other pre-scripts for consistency.

Labels: PR hardens GHA workflow command injection surface in pre-code scripts — security-relevant change


Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR

fullsend-ai-review[bot]

This comment was marked as outdated.

@fullsend-ai-coder

fullsend-ai-coder Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

🤖 Finished Fix · ✅ Success · Started 9:03 PM UTC · Completed 9:06 PM UTC
Commit: 3bfce3f · View workflow run →

@fullsend-ai-coder

Copy link
Copy Markdown
Contributor

🔧 Fix agent — iteration 1 (bot-triggered)

Both review findings are governance/procedural or out-of-scope suggestions rather than code defects. Finding 1 (protected-path) requires human approval, which the fix agent cannot provide. Finding 2 (output-consistency) suggests extending changes to other pre-scripts, which is out of scope for this PR. No code changes were made.

Disagreed (2):

  1. protected-path: scripts/pre-code.sh and scripts/pre-code.src.sh require human approval: This finding is a governance requirement, not a code defect. The fix agent cannot grant or obtain human approval — that is outside the agent's scope. The PR already has the 'security' label and a clear security-hardening rationale in its description. A human reviewer must approve protected-path changes; no code change addresses this finding.
  2. output-consistency: apply validate-then-notice pattern to other pre-scripts for consistency: Extending the validate-then-notice pattern to pre-triage.sh, pre-review.sh, and pre-prioritize.sh is out of scope for this PR, which specifically addresses pre-code.sh/pre-code.src.sh. Per AGENTS.md §3 (Surgical changes), every changed line must trace to the issue scope. The reviewer acknowledged this is intentional security hardening. Modifying additional pre-scripts would expand the PR's scope and touch more protected-path files without authorization.

Tests: passed

Next steps:

  • /fs-review — request a re-review of the changes
  • /fs-fix <your instruction> — run another fix pass with specific guidance
  • Push commits directly — review re-runs automatically on push
    Updated by fullsend fix agent

@waynesun09 waynesun09 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review finding

[HIGH] scripts/pre-code.src.sh (lines 28, 33, 38, 46, 50) — Validation-failure ::error:: lines still echo raw untrusted input, the same injection class this PR claims to fix

This PR moves the GITHUB_ISSUE_URL ::notice:: to after validation and stops echoing raw COMMENT_BODY, specifically so unvalidated/attacker-controlled values never reach GHA workflow-command parsing. But five untouched lines in the same validation block still interpolate the raw, already-known-invalid value directly into ::error:: lines:

  • line 28: echo "::error::ISSUE_NUMBER must be a positive integer, got: '${ISSUE_NUMBER:-}'"
  • line 33: REPO_FULL_NAME format error
  • line 38: GITHUB_ISSUE_URL format error
  • line 46 and line 50: the URL-vs-input cross-check mismatches

Each of these fires exactly when the value has failed its regex check or cross-check — i.e., precisely the malformed/malicious event_payload class the script's own header comment says it exists to guard against. GHA's workflow-command parser scans every stdout line for a leading ::; a value containing an embedded literal newline followed by ::something:: (e.g. ISSUE_NUMBER, REPO_FULL_NAME, or GITHUB_ISSUE_URL supplied via workflow_dispatch with such content) would inject a second workflow command on the following log line, regardless of being nested inside another ::error:: message.

These lines are unchanged by the PR's diff (and not visible in the diff's context window, hence this is a whole-review comment rather than an inline one) and are not covered by any existing review comment — those only address the protected-path warning, missing test coverage, and the other pre-scripts' notice ordering.

Suggestion: Apply the same treatment used for COMMENT_BODY here — don't interpolate raw untrusted values directly into ::error:: lines. Strip/replace embedded newlines and ::-like sequences before interpolating (e.g. ${VALUE//$'\n'/ }), or follow GitHub's documented escaping for workflow commands (%%25, \r%0D, \n%0A), applied consistently across all five sites (scripts/pre-code.src.sh:28,33,38,46,50, mirrored in scripts/pre-code.sh).

Five ::error:: lines interpolated raw, already-invalid ISSUE_NUMBER,
REPO_FULL_NAME, and GITHUB_ISSUE_URL values — the same workflow-command
injection class this PR already fixed for the ::notice:: line and
COMMENT_BODY logging. Route them through the existing gha_echo sanitizer
(scripts/lib/post-failure-report.lib.sh) instead of hand-rolling a new
one, matching the convention pr-assignee.lib.sh already documents.

Also adds regression tests for the ::notice:: reordering and the
COMMENT_BODY length logging that this PR introduced but didn't test.

Assisted-by: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Ralph Bean <rbean@redhat.com>
@ralphbean

Copy link
Copy Markdown
Member Author

@waynesun09 good eye — same injection class, just five lines that weren't in this diff's context window. Pushed 90f03f3, which routes those through gha_echo (from post-failure-report.lib.sh) instead of a hand-rolled sanitizer — matches what pr-assignee.lib.sh already does for the same reason. Added regression tests too, since the earlier commits in this PR didn't have any for the notice-reordering or the COMMENT_BODY-length change either.

Also turned up the same pattern in pre-review.sh and pre-fix.sh while looking at this — filed #598 to track those separately since they're outside this PR's scope.

@fullsend-ai-review

fullsend-ai-review Bot commented Jul 31, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 1:29 PM UTC · Completed 1:46 PM UTC
Commit: 90f03f3 · View workflow run →

fullsend-ai-review[bot]

This comment was marked as outdated.

@fullsend-ai-coder

fullsend-ai-coder Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

🤖 Finished Fix · ✅ Success · Started 1:48 PM UTC · Completed 1:54 PM UTC
Commit: 90f03f3 · View workflow run →

Route CODE_FORCE through sanitize_gha_log_output before interpolation
in the force-override debug echo, closing the remaining unsanitized
workflow-command injection vector flagged in review.

Addresses review feedback on #576
@fullsend-ai-coder

Copy link
Copy Markdown
Contributor

🔧 Fix agent — iteration 1 (bot-triggered)

Addressed 1 of 3 review findings with a code fix. Disagreed with 2: [protected-path] requires human approval (procedural, not a code fix), and [edge-case] is an acknowledged security-over-debuggability tradeoff with no remediation requested. The [injection] finding was valid — CODE_FORCE was the last unsanitized interpolation in the force-override debug log; now routed through sanitize_gha_log_output consistent with the PR's existing COMMENT_BODY hardening pattern.

Fixed (1):

  1. [injection] CODE_FORCE interpolated via plain echo in force-override debug log (scripts/pre-code.src.sh): Routed CODE_FORCE through sanitize_gha_log_output (from post-failure-report.lib.sh, already sourced) before interpolation in the force-override debug echo. Applied the same change to the bundled pre-code.sh. Added regression test 'code-force-sanitized-in-log' verifying that :: in CODE_FORCE is stripped from stdout.

Disagreed (2):

  1. [protected-path] All changed files under scripts/ require human approval: This is a governance/procedural requirement, not a code defect. The fix agent cannot provide human approval. A human maintainer (ralphbean) has already reviewed and commented on this PR. Protected-path enforcement is handled by post-review.sh — human approval must come from a human reviewer.
  2. [edge-case] _sanitize_workflow_value strips :: from error messages, losing diagnostic fidelity: The reviewer explicitly notes this is 'an inherent security-over-debuggability tradeoff' and provides no remediation. The behavior is by design — the unsanitized value failed validation specifically because it's malformed, so stripping :: before displaying it in the error message is the correct security posture. Preserving the raw value would reintroduce the injection vector this PR closes.

Tests: passed

Next steps:

  • /fs-review — request a re-review of the changes
  • /fs-fix <your instruction> — run another fix pass with specific guidance
  • Push commits directly — review re-runs automatically on push
    Updated by fullsend fix agent

@fullsend-ai-review

fullsend-ai-review Bot commented Jul 31, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 1:56 PM UTC · Completed 2:08 PM UTC
Commit: 16d120d · View workflow run →

fullsend-ai-review[bot]

This comment was marked as outdated.

@fullsend-ai-coder

fullsend-ai-coder Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

🤖 Finished Fix · ✅ Success · Started 2:09 PM UTC · Completed 2:15 PM UTC
Commit: 16d120d · View workflow run →

@fullsend-ai-coder

Copy link
Copy Markdown
Contributor

🔧 Fix agent — iteration 2 (bot-triggered)

Disagreed with both remaining review findings. [protected-path] is a procedural governance requirement that requires human approval — not a code fix. [edge-case] is an acknowledged security-over-debuggability tradeoff with no remediation requested by the reviewer. The [injection] finding from the previous review cycle was already fixed in iteration 1 and is no longer raised.

Disagreed (2):

  1. [protected-path] All changed files under scripts/ require human approval: This is a governance/procedural requirement, not a code defect. The fix agent cannot provide human approval — that must come from a human reviewer. Protected-path enforcement is handled by post-review.sh. A human maintainer (ralphbean) has already reviewed and commented on this PR.
  2. [edge-case] _sanitize_workflow_value strips :: from error messages, losing diagnostic fidelity: The reviewer explicitly acknowledges this is 'an inherent security-over-debuggability tradeoff' and provides no remediation. The behavior is by design: the unsanitized value failed validation specifically because it's malformed, so stripping :: before displaying it in the error message is the correct security posture. Additionally, the script exits with a non-zero exit code, so the rejection is unambiguous regardless of the sanitized message content. Modifying _sanitize_workflow_value would change the shared library (lib/post-failure-report.lib.sh) used by multiple scripts — expanding scope beyond this PR's security hardening of pre-code.

Tests: passed

Next steps:

  • /fs-review — request a re-review of the changes
  • /fs-fix <your instruction> — run another fix pass with specific guidance
  • Push commits directly — review re-runs automatically on push
    Updated by fullsend fix agent

@waynesun09 waynesun09 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review finding

[HIGH] scripts/pre-review.sh (lines 17, 22, 27, 35, 39) and scripts/pre-fix.sh (lines 33, 38) — Identical unpatched workflow-command injection remains in sibling pre-scripts

This PR's stated purpose is to harden pre-script GHA workflow-command injection surface, and it correctly converts pre-code.src.sh's ::error:: validation lines to gha_echo. But pre-review.sh (PR_NUMBER, REPO_FULL_NAME, GITHUB_PR_URL, and the two cross-check mismatch lines) and pre-fix.sh (PR_NUMBER, REPO_FULL_NAME) still use raw echo "::error::... got: '${VALUE:-}'", interpolating the same not-yet-trusted, just-failed-validation input directly into a ::-prefixed line. Verified against file contents at head commit 16d120d — both files are untouched by this PR (hence a whole-review comment rather than inline, since neither file appears in this PR's diff) and neither is bundled from a .src.sh source. This is the exact vulnerability class the PR exists to fix, left open in two of the other pre-scripts, and isn't covered by the existing maintainer response (which only scoped out the unrelated ::notice:: reordering cosmetic issue, not this injection vector).

Suggestion: Either narrow the PR title/description to scope the fix explicitly to pre-code (noting the identical exposure remains elsewhere), or apply the same gha_echo-based sanitization to the equivalent lines in pre-review.sh and pre-fix.sh in this PR or an immediate follow-up tracked by an issue.


Review finding

[MEDIUM] PR description Summary omits half of the actual diff's hardening changes

Verified via gh pr view 576: the Summary lists only two changes (move ::notice:: after validation; replace raw COMMENT_BODY echo with length). The actual diff at head also converts all five ::error:: validation lines to gha_echo and sanitizes CODE_FORCE in the force-override debug log — added reactively during review but never folded back into the description.

Suggestion: Update the PR Summary to list all four hardening changes actually present in the diff before merge, so reviewers/future readers see the true scope.

Comment thread scripts/pre-code.src.sh Outdated
Comment thread scripts/pre-code-test.sh
waynesun09 pointed out that pre-code.src.sh was sourcing the entire
post-failure-report.lib.sh (347 lines: gh comment posting, PUSH_TOKEN/
GH_TOKEN/PEM redaction, post_fail_to_issue/report_post_failure_to_issue)
just to reuse gha_echo/sanitize_gha_log_output. That grew the bundled
pre-code.sh from 230 to 584 lines and put comment-posting helpers in
scope at the most trust-sensitive point in the pipeline (runs before
sandbox creation).

Pull gha_echo, sanitize_gha_log_output, print_sanitized_gha_log,
sanitize_comment_workflow_commands, and _sanitize_workflow_value into a
new scripts/lib/gha-log-sanitize.lib.sh with no gh/comment-posting
logic. post-failure-report.lib.sh now sources that lib instead of
defining these itself, and pre-code.src.sh sources it directly instead
of the whole post-failure-report.lib.sh. Bundled pre-code.sh drops back
to 302 lines.

Also teaches bundle-sh.sh to strip a ${LIB_DIR}/ prefix the same way it
already strips ${SCRIPT_DIR}/ and ${SCRIPT_DIR_POST}/, since a library
sourcing a sibling library needs to resolve relative to its own
directory (via BASH_SOURCE) rather than the top-level script's
directory — the caller's SCRIPT_DIR/SCRIPT_DIR_POST convention isn't
reliable from inside a sourced lib (post-code.src.sh/post-fix.src.sh
use SCRIPT_DIR_POST, not SCRIPT_DIR, and referencing the wrong one
under set -u aborts the script with "unbound variable").

Assisted-by: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Ralph Bean <rbean@redhat.com>
waynesun09 noted the injection-hardening regression tests all assert
negative/absence cases only — nothing confirms the ::notice:: line
still fires on the validation-success path after the reordering. A
future edit that deleted the notice entirely (not just reordered it)
would pass the suite undetected.

Verified the new test fails when the notice line is removed, and
passes against the current script.

Assisted-by: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Ralph Bean <rbean@redhat.com>
@fullsend-ai-review

fullsend-ai-review Bot commented Jul 31, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 6:14 PM UTC · Completed 6:29 PM UTC
Commit: 69808f3 · View workflow run →

fullsend-ai-review[bot]

This comment was marked as outdated.

@fullsend-ai-coder

fullsend-ai-coder Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

🤖 Finished Fix · ✅ Success · Started 6:31 PM UTC · Completed 6:38 PM UTC
Commit: 69808f3 · View workflow run →

…ce through it

- Add defence-in-depth sanitization of the level parameter in gha_echo
  to strip :: sequences, future-proofing against non-literal callers
- Route the "Found existing human PR" notice through gha_echo instead
  of raw echo for consistency with the PR's hardening goal
- Rebuild bundled scripts to propagate the library change

Addresses review feedback on #576
@fullsend-ai-coder

Copy link
Copy Markdown
Contributor

🔧 Fix agent — iteration 2 (bot-triggered)

Fixed 2 of 4 findings: added defence-in-depth sanitization of the gha_echo level parameter and routed the remaining raw ::notice:: through gha_echo. Disagreed with 2 findings: protected-path is a process concern requiring human approval, and the edge-case diagnostic fidelity tradeoff was acknowledged by the reviewer as inherent. All pre-code tests pass. Bundled scripts rebuilt.

Fixed (2):

  1. workflow-command-injection: gha_echo does not sanitize the level parameter (scripts/lib/gha-log-sanitize.lib.sh): Added level="${level//::/}" to strip :: sequences from the level parameter before interpolation into the printf format string. This defence-in-depth change ensures future callers passing non-literal level values cannot inject workflow commands.
  2. workflow-command-injection: Found existing human PR notice uses raw echo instead of gha_echo (scripts/pre-code.src.sh): Changed the '::notice::Found existing human PR' line from raw echo to gha_echo for consistency with the PR's hardening goal. The values interpolated (PR number and author login) are practically safe, but routing through gha_echo maintains the principle of never emitting raw :: workflow commands with interpolated values.

Disagreed (2):

  1. protected-path: all changed files under scripts/ require human approval: This is a process/policy requirement, not a code defect. The PR intentionally modifies scripts/ for security hardening. Human approval must come from a human reviewer — the fix agent cannot grant it.
  2. edge-case: sanitized error message loses diagnostic fidelity when :: is stripped from ISSUE_NUMBER: The reviewer explicitly acknowledges this is 'an inherent security-over-debuggability tradeoff, covered by regression test.' No code change was requested — this is an observation, not an action item.

Tests: passed

Next steps:

  • /fs-review — request a re-review of the changes
  • /fs-fix <your instruction> — run another fix pass with specific guidance
  • Push commits directly — review re-runs automatically on push
    Updated by fullsend fix agent

@fullsend-ai-review

fullsend-ai-review Bot commented Jul 31, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 6:39 PM UTC · Completed 6:56 PM UTC
Commit: f52a4a0 · View workflow run →

@fullsend-ai-review fullsend-ai-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See the review comment for full details.

Comment thread scripts/pre-code.src.sh
@fullsend-ai-coder

fullsend-ai-coder Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

🤖 Finished Fix · ✅ Success · Started 6:58 PM UTC · Completed 7:02 PM UTC
Commit: f52a4a0 · View workflow run →

@fullsend-ai-coder

Copy link
Copy Markdown
Contributor

🔧 Fix agent — iteration 3 (bot-triggered)

Both review findings are non-actionable by the fix agent. The protected-path finding is a governance policy requiring human approval (outside fix agent scope). The edge-case finding is an acknowledged security-over-debuggability tradeoff already covered by a regression test. All previous code-level findings (CODE_FORCE sanitization, gha_echo level parameter, notice routing) were addressed in earlier iterations. No code changes made.

Disagreed (2):

  1. protected-path: all changed files under scripts/ require human approval: This is a governance policy requirement, not a code defect. The fix agent cannot grant human approval — a human reviewer must approve protected-path changes. This finding has been consistent across all review iterations and is working as designed: the review agent correctly flags protected paths so a human is aware.
  2. edge-case: sanitized ISSUE_NUMBER loses :: making error message look like valid integer: The reviewer explicitly acknowledges this as 'an inherent security-over-debuggability tradeoff, covered by regression test.' The regression test (pre-code-test.sh, test 'error-line-strips-workflow-cmd-chars') verifies the exact behavior: ISSUE_NUMBER=12::34 is rejected (correct), and the sanitized error message shows '1234' without :: (expected). No code change is requested or needed — the tradeoff is intentional and documented.

Tests: passed

Next steps:

  • /fs-review — request a re-review of the changes
  • /fs-fix <your instruction> — run another fix pass with specific guidance
  • Push commits directly — review re-runs automatically on push
    Updated by fullsend fix agent

@ralphbean

Copy link
Copy Markdown
Member Author

@waynesun09 on the sibling scripts — filed #598 before this review came in to track pre-review.sh/pre-fix.sh separately, keeping this PR scoped to pre-code. On the description — good catch, updated it to list all four hardening changes actually in the diff.

Comment thread scripts/lib/gha-log-sanitize.lib.sh Outdated
Comment thread scripts/pre-code-test.sh
_sanitize_workflow_value stripped literal "::" before stripping the
"%0A"/"%0D" text tokens, so a value like ":%0A:notice:%0A:PWNED" had
no "::" substring until the percent tokens were removed — at which
point the flanking colons collapsed back into a live "::notice::"
workflow command, defeating the injection hardening this PR set out
to add. Loop the substitutions to a fixed point so no strip order can
reconstruct a stripped token, and also collapse real embedded CR/LF
bytes, since GHA's runner parses stdout line-by-line and a literal
newline would start a new physical line the "::" check never sees.

Adds regression tests in post-failure-report-test.sh reproducing both
the percent-token reconstruction and the newline+reconstruction case.

Assisted-by: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Ralph Bean <rbean@redhat.com>

@waynesun09 waynesun09 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review-only sweep: 2 findings on the CR/LF and sibling-function gaps in the sanitizer, both verified reproducible against the current head commit.

done
# GHA's runner parses stdout line-by-line; a literal CR/LF byte would start
# a new physical line the "::" strip above never sees.
value="${value//$'\r'/}"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[CRITICAL] _sanitize_workflow_value strips CR/LF only once, after the fixed-point loop, letting a raw CR byte reconstruct a live :: command

The fixed-point loop above (lines 27-35) correctly re-strips ::/%0A/%0a/%0D/%0d text tokens until convergence, closing the prior strip-order bypass. But the literal CR-byte strip on this line and the LF-to-space substitution on the next line run exactly once, after the loop has already exited, and are never re-checked. A value that uses a real CR byte (not the %0D text token) to separate colons produces no :: and no percent-token substring, so the loop sees zero changes and breaks immediately; the trailing one-shot CR strip then collapses the flanking colons into a live workflow command with no further pass to catch it.

I verified this against the current head commit by sourcing the file directly:

input=$':\r:notice:\r:PWNED-INJECTED-VIA-CR'
sanitize_gha_log_output "$input"   # -> "::notice::PWNED-INJECTED-VIA-CR"

This is reachable through every caller of sanitize_gha_log_output/gha_echo/print_sanitized_gha_log, including the pre/post-script sinks this PR set out to close.

Suggestion: move the CR/LF strip lines inside the while true; ... done loop, before the prev/value equality check, so a :: reassembled by CR/LF removal triggers another iteration and gets caught by the token strip. Add a regression test using a literal $'\r' byte between colons, mirroring the existing %0A-reconstruction test.


# Neutralize line-start GHA workflow commands in comment bodies without
# stripping mid-string :: (e.g. std::string in compiler output).
sanitize_comment_workflow_commands() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MEDIUM] sanitize_comment_workflow_commands (sibling function) still has the same percent-token reconstruction bug the fixed-point loop above was added to fix

_sanitize_workflow_value was hardened with a fixed-point loop specifically because "removing one token can reassemble another" (comment above, lines 23-26). This sibling function does a single non-looped pass — one leading-::cmd:: sed strip, then a one-shot %0A/%0a/%0D/%0d removal — and wasn't updated. Verified against the current head:

sanitize_comment_workflow_commands ':%0A:notice:%0A:PWNED-INJECTED'  # -> "::notice::PWNED-INJECTED"

because the leading-command sed check runs before there's any literal :: to match, then the percent-strip collapses the flanking colons afterward with no re-check.

I traced the only call site (sanitize_failure_detail in post-failure-report.lib.sh) and its output goes only to gh issue comment/gh pr comment, never to actual GHA runner stdout — so this doesn't become a live, GHA-parsed workflow command (hence MEDIUM rather than the sibling's CRITICAL). The impact is a forged-looking ::notice::/::error::-prefixed line inside an otherwise-trusted CI-failure comment on a public issue/PR — content spoofing rather than command injection.

Separately, the leading-command allow-list regex (^::(warning|error|notice|debug|group|endgroup)::) omits other real GHA workflow commands (stop-commands, add-mask, save-state, set-output, set-env, add-path, echo), so a value starting with e.g. ::add-mask:: wouldn't even hit the sed strip.

Suggestion: apply the same fixed-point-loop pattern to the token stripping here (keeping the distinct leading-::cmd::-only first pass, re-run after token removal converges), and broaden the allow-list regex to a generic ^::[A-Za-z-]+:: prefix match. Add a regression test mirroring the sibling's %0A-reconstruction test.

@ralphbean

Copy link
Copy Markdown
Member Author

Just dropping this for now. I need to focus on other issues.

@ralphbean ralphbean closed this Aug 10, 2026
@fullsend-ai-retro

fullsend-ai-retro Bot commented Aug 10, 2026

Copy link
Copy Markdown

🤖 Finished Retro · ✅ Success · Started 4:34 PM UTC · Completed 4:49 PM UTC

Commit: b445ee1 · View workflow run →

@fullsend-ai-retro

Copy link
Copy Markdown

Retro: PR #576 — pre-code GHA injection hardening

This security-hardening PR went through 5 review/fix cycles over 2 days (Jul 30 – Jul 31) before being closed unmerged on Aug 10. The author (ralphbean) dropped it citing the need to focus on other work. Two CRITICAL sanitizer bypass vulnerabilities identified by human reviewer waynesun09 on Aug 6 remained unaddressed.

Review quality gap

The review agent's security sub-agent found only LOW-severity issues across all 5 review passes (output-consistency, edge-case diagnostic fidelity, CODE_FORCE injection, gha_echo level param). The human reviewer found 7 distinct findings including 2 CRITICAL bypass vulnerabilities:

  • CRITICAL: _sanitize_workflow_value strip-order bypass — stripping :: before %0A tokens allows reconstruction via :%0A:notice:%0A:PWNED (comment)
  • CRITICAL: CR-byte bypass — raw CR bytes not covered by fixed-point loop, $':\r:notice:\r:PWNED' reconstructs live ::notice:: (comment)
  • MEDIUM: Sibling function sanitize_comment_workflow_commands has the same percent-token reconstruction bug
  • MEDIUM: Entire post-failure-report.lib.sh (~347 lines) bundled for 2 small helpers
  • MEDIUM: No positive-path regression test for relocated ::notice::
  • MEDIUM: Regression tests only cover single-line :: payloads, not embedded newlines
  • HIGH: 5 validation-failure ::error:: lines still echo raw untrusted input

The review agent found 0 of these 7 findings. The security sub-agent's existing prompt covers GHA workflow command injection but did not reason about substitution-chain interactions or adversarially test the sanitizer implementation.

Rework rate

Of 5 fix agent runs, 3 produced zero code changes — the fix agent correctly identified all remaining findings as governance concerns (protected-path) or acknowledged tradeoffs (edge-case) it couldn't resolve. These 3 runs consumed approximately 15 minutes of compute with no value.

The edge-case finding (diagnostic fidelity when :: is stripped from ISSUE_NUMBER) was raised 4 times across review cycles despite the author explicitly declining it each time.

Evidence for existing open issues

No new proposals

All improvement opportunities identified in this retro are already tracked by open issues. The existing issues, if implemented, would address the review quality gap (agents#282, #535), the loop friction (fullsend#1068, agents#106, #680), and the adjacent-code blind spot (agents#634).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants