Skip to content

gh-aw: github.event.* command injection via heredoc-blind template guardrails (MCP config, all engines)

High
pelikhan published GHSA-796c-cr8h-rr49 Aug 25, 2026

Package

actions github/gh-aw (GitHub 操作)

Affected versions

<= 0.82.4 (all released versions; verified on main at commit 966ccb8, 2026-07-12)

Patched versions

None

Description

Summary

gh-aw's template-injection guardrails skip heredoc bodies before scanning them,
on the incorrect premise that heredoc content is not executed. gh-aw then emits
its MCP config as an unquoted heredoc on every engine, and renders custom MCP
server fields into it verbatim. A ${{ github.event.* }} in any custom MCP
field reaches the unquoted heredoc and is command-substituted by bash at
runtime, giving code execution in the agent job. The build succeeds with no
warning.

I verified this against current main at commit 966ccb8 (2026-07-12). It was
first verified at bdfdb8e (2026-06-24, ~v0.80.9). The one intervening commit
that touched this area (PR #44454) widened the exposure rather than closing it
(see Details).

Details

removeHeredocContent (pkg/workflow/template_injection_utils.go:71-82) strips
both quoted and unquoted heredoc bodies before any guardrail scans. The premise
is wrong: GitHub 操作 substitutes ${{ ... }} into the script text before
bash parses it, a quoted delimiter does not stop the substitution, and a
multi-line attacker value can contain a line equal to the delimiter to close the
heredoc early and run the following lines. Stripping the body blinds all three
run-step defenses: the auto-fixer that hoists expressions into env vars, and
both compile validators.

gh-aw emits the MCP config as an unquoted heredoc on every engine. The gateway
JSON config is cat << GH_AW_MCP_CONFIG_<hex>_EOF | "$GH_AW_NODE" ...
(pkg/workflow/mcp_renderer.go:241, delimiter not single-quoted). The codex
TOML config is the same pattern (pkg/workflow/codex_mcp.go:103,144,156).
Custom MCP server fields (command, args, url, headers, env) are written
verbatim with raw fmt.Fprintf and no escaping
(pkg/workflow/mcp_config_custom.go; only secrets.* values are rewritten). A
${{ github.event.* }} in any of those fields lands in the unquoted heredoc and
is command-substituted at runtime.

The surrounding controls do not cover it. The env-indirection helper rewrites
only secrets.*, env.*, and github.workspace, never github.event.*
(pkg/workflow/secret_extraction.go:219-242,324-346). validateExpressionSafety
scans only the markdown body, never the frontmatter mcp-servers config
(pkg/workflow/compiler_validators.go:32).

Intervening commit: PR #44454 ("skip heredoc content in run-block expression
scan") extended the same heredoc-skipping to the one raw-text guardrail path that
previously did not understand heredocs
(pkg/workflow/template_injection_validation.go:178-246). Both scanners now
deliberately skip heredoc bodies, so the exposure is marginally wider than at
bdfdb8e, not narrower.

PoC

I compiled a workflow with
url: "https://api.example.com/${{ github.event.issue.title }}" plus a headers
entry using github.event.issue.body. The build succeeds and the raw
expressions land in the unquoted MCP heredoc (confirmed for claude/codex/copilot).
Running the compiled step through bash with an issue title of
$(echo PWNED_F11 > marker; echo INJECTED) executed the command.

Impact

Command execution in the agent job, which holds GITHUB_TOKEN and the engine API
keys. No adopter interpolates github.event.* into a custom MCP field today
(custom MCP servers are rare), so this is a framework fix in a core trust
boundary rather than an active mass-exploit. The same heredoc blind spot also
covers any author run: step that contains a heredoc with ${{ github.event.* }}:
it compiles with no warning and the raw expression survives.

CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H (8.3). Up to 10.0 (Critical) under
roles: all with an issue/PR trigger, where any external user reaches it
directly.

Suggested fix

Stop treating heredoc bodies as non-executable for expression scanning: either
extract ${{ }} from heredoc bodies into env vars, or make the regression
guardrail not strip heredocs so it hard-fails on any expression left inside one.
Single-quote the framework's own MCP-config heredoc delimiters and pass framework
variables through the gateway env.

Severity

High

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
网络
Attack complexity
High
Privileges required
Low
User interaction
None
Scope
Changed
Confidentiality
High
Integrity
High
Availability
High

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H

CVE ID

No known CVE

Weaknesses

Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')

The product constructs all or part of an OS command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended OS command when it is sent to a downstream component. Learn more on MITRE.

Credits