You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Single-agent VulnHunter run over a pre-ranked 40-file scope (Go CLI/compiler + actions/setup JS). Applied the injection phase, then falsified every candidate with the adversarial verification phase. One finding survived.
Summary
Count
Files in scope
40
Candidates raised
5
Confirmed after falsification
1
Eliminated / downgraded
4
The codebase is well hardened on the paths reviewed — dedicated ValidateGitRef / ValidateGitPath / validateExecArgument / validateDockerImageRef guards, argv-only exec.Command (no shell), and -- end-of-options separators are used consistently. The one gap is a hand-rolled git mailbox patch built from untrusted pull request text.
VULN-001 — Git mailbox/patch injection via attacker-controlled PR body
Type: CWE-74 — improper neutralization of special elements in output used by a downstream component
Severity: Medium
Attacker path.gh aw pr transfer <pr-url> → fetchPRInfo (line 188) reads .body verbatim from the GitHub REST API for the named PR. Anyone who can open a PR on the source repository controls it. createPatchFromPR then interpolates it straight into a git mailbox patch:
fmt.Fprintf(&patchBuilder, "Subject: [PATCH] %s\n", prInfo.Title)
patchBuilder.WriteString("\n")
ifprInfo.Body!="" {
fmt.Fprintf(&patchBuilder, "%s\n", prInfo.Body) // line 250 — unsanitizedpatchBuilder.WriteString("\n")
}
...patchBuilder.WriteString("---\n") // the tool's own separatorpatchBuilder.Write(diffContent) // the genuine PR diff
The file is fed to git am (line 340), falling back to git apply --3way / --ignore-space-change / --reject (lines 361, 368, 374).
Why it works.git am / git mailinfo end the commit message at the first line consisting of exactly--- and treat the remainder as the patch. Because the body is emitted before the tool's own --- and is never scanned for that sequence, a crafted body injects its own patch section. git apply skips non-diff text between diff --git headers, so injected hunks are applied in addition to the genuine diff that follows.
A new file mode hunk always applies cleanly, so the attack does not depend on the target repository's file state.
gh aw pr transfer then writes these changes into the target repository, commits, pushes a branch, and opens a PR (applyPatchToRepo → createTransferPR). The documented use case — "migrating work from trial repositories to production repositories", with examples using gh-aw-trial/repo — is exactly the low-trust-source → high-trust-target direction that makes this reachable.
Why this is credible after falsification
Grepped every use of prInfo.Body (lines 250, 400-401, 512): no --- stripping, no escaping, no --scissors anywhere between the API read and the patch write.
createPatchFromPR has a single caller; patchFile has a single consumer. git am and all three git apply fallbacks parse the same injected content, so no fallback path avoids it.
Not eliminated by "the operator reviews the resulting PR": the injected hunks are absent from the source PR the operator inspected before running the command. Downstream review is a mitigating control, not a defense at this sink — which is why this is rated Medium rather than High.
Remediation (in order of preference):
Stop embedding the body in the patch. Apply the raw gh pr diff output with git apply (no mail headers), then create the commit separately via git commit -m / -F -, where the body is an argument that can never be reparsed as patch content. The code already has this path at lines 391-409 — make it the only path.
If mailbox format must be kept, use git am --scissors with an explicit -- >8 -- line, or neutralize any body line matching ^-{3}\s*$ before writing.
Defense in depth: after applying, assert the changed-file set equals gh pr diff --name-only and abort on divergence.
Option 1 removes the whole class; escaping only the --- sequence is a narrower fix.
Candidates raised and eliminated (not vulnerabilities)
Candidate
Verdict
Subject: header injection via PR title — pr_command.go:246
Not confirmed. GitHub PR titles are single-line; CRLF injection unproven. Worth hardening alongside the fix above, but no demonstrated attack.
gh api path injection via item.repo — actions/setup/js/evaluate_outcomes.cjs:96-104
Not confirmed.item.repo comes from agent-produced artifacts and is unvalidated, but the sink is execFileSync (no shell), GET-only, same-host, and the response drives only boolean classification. No exfiltration oracle. Hardening suggestion: validate against ^[\w.-]+/[\w.-]+$.
False positive (Gate 3). The blocklist (require(, eval(, child_process, ...) is trivially bypassable (require (, "child_"+"process", (()=>{}).constructor), but the script author is the workflow author, who already controls steps:/run: in the same frontmatter. No new capability — a guardrail, not a trust boundary.
Single-agent VulnHunter run over a pre-ranked 40-file scope (Go CLI/compiler +
actions/setupJS). Applied the injection phase, then falsified every candidate with the adversarial verification phase. One finding survived.Summary
The codebase is well hardened on the paths reviewed — dedicated
ValidateGitRef/ValidateGitPath/validateExecArgument/validateDockerImageRefguards, argv-onlyexec.Command(no shell), and--end-of-options separators are used consistently. The one gap is a hand-rolled git mailbox patch built from untrusted pull request text.VULN-001 — Git mailbox/patch injection via attacker-controlled PR body
pkg/cli/pr_command.go—createPatchFromPR(line 250) →applyPatchToRepo(lines 340, 361, 368, 374)Attacker path.
gh aw pr transfer <pr-url>→fetchPRInfo(line 188) reads.bodyverbatim from the GitHub REST API for the named PR. Anyone who can open a PR on the source repository controls it.createPatchFromPRthen interpolates it straight into a git mailbox patch:The file is fed to
git am(line 340), falling back togit apply --3way/--ignore-space-change/--reject(lines 361, 368, 374).Why it works.
git am/git mailinfoend the commit message at the first line consisting of exactly---and treat the remainder as the patch. Because the body is emitted before the tool's own---and is never scanned for that sequence, a crafted body injects its own patch section.git applyskips non-diff text betweendiff --githeaders, so injected hunks are applied in addition to the genuine diff that follows.Proof-of-concept PR body and impact
A
new file modehunk always applies cleanly, so the attack does not depend on the target repository's file state.gh aw pr transferthen writes these changes into the target repository, commits, pushes a branch, and opens a PR (applyPatchToRepo→createTransferPR). The documented use case — "migrating work from trial repositories to production repositories", with examples usinggh-aw-trial/repo— is exactly the low-trust-source → high-trust-target direction that makes this reachable.Why this is credible after falsification
prInfo.Body(lines 250, 400-401, 512): no---stripping, no escaping, no--scissorsanywhere between the API read and the patch write.createPatchFromPRhas a single caller;patchFilehas a single consumer.git amand all threegit applyfallbacks parse the same injected content, so no fallback path avoids it.Remediation (in order of preference):
gh pr diffoutput withgit apply(no mail headers), then create the commit separately viagit commit -m/-F -, where the body is an argument that can never be reparsed as patch content. The code already has this path at lines 391-409 — make it the only path.git am --scissorswith an explicit-- >8 --line, or neutralize any body line matching^-{3}\s*$before writing.gh pr diff --name-onlyand abort on divergence.Option 1 removes the whole class; escaping only the
---sequence is a narrower fix.Candidates raised and eliminated (not vulnerabilities)
Subject:header injection via PR title —pr_command.go:246gh apipath injection viaitem.repo—actions/setup/js/evaluate_outcomes.cjs:96-104item.repocomes from agent-produced artifacts and is unvalidated, but the sink isexecFileSync(no shell), GET-only, same-host, and the response drives only boolean classification. No exfiltration oracle. Hardening suggestion: validate against^[\w.-]+/[\w.-]+$.pkg/workflow/graders_config.go:424require(,eval(,child_process, ...) is trivially bypassable (require (,"child_"+"process",(()=>{}).constructor), but the script author is the workflow author, who already controlssteps:/run:in the same frontmatter. No new capability — a guardrail, not a trust boundary.actions/setup/js/validate_secrets.cjs:156-166owner/repoderive fromcontext.repo(i.e.GITHUB_REPOSITORY), not attacker-controlled.Warning
Firewall blocked 1 domain
The following domain was blocked by the firewall during workflow execution:
api.anthropic.comTo allow these domains, add them to the
network.allowedlist in your workflow frontmatter:See Network Configuration for more information.