Summary
The MCP CLI wrapper scripts generated by actions/setup/js/mount_mcp_as_cli.cjs embed the MCP gateway API key in plaintext and are written with world-readable/executable permissions (mode: 0o755) into ${RUNNER_TEMP}/gh-aw/mcp-cli/bin. The gateway API key is a security-sensitive credential: the project's own threat model (see convert_gateway_config_copilot.sh) states that anyone able to read this key can "bypass the --allowed-tools constraint by issuing raw JSON-RPC calls directly to the gateway." Elsewhere in the same action the key is deliberately protected with chmod 600, so this file-permission handling is inconsistent and weakens the intended containment boundary between the agent and the gateway.
Affected Files
| File |
Line(s) |
Pattern |
actions/setup/js/mount_mcp_as_cli.cjs |
622 |
fs.writeFileSync(scriptPath, generateCLIWrapperScript(name, containerUrl, toolsFile, apiKey, bridgeScript), { mode: 0o755 }) |
actions/setup/js/mount_mcp_as_cli.cjs |
~344 |
wrapper body embeds --api-key "${safeApiKey}" in plaintext |
For comparison, the intended hardening for the same credential:
| File |
Line(s) |
Pattern |
actions/setup/sh/convert_gateway_config_copilot.sh |
106 |
chmod 600 "$COPILOT_CONFIG_PATH" (+ umask 077) |
actions/setup/sh/convert_gateway_config_codex.sh |
99 |
chmod 600 /tmp/gh-aw/mcp-config/config.toml |
Risk
Severity: Medium
Attack vector: Any process or user account that can read files under ${RUNNER_TEMP}/gh-aw/mcp-cli/bin (mode 0o755 files inside a 0o555 directory — both world-readable) can cat a wrapper script and recover the gateway API key. On multi-tenant, self-hosted, or container-shared runners where the workflow does not have exclusive ownership of the runner temp directory, an unprivileged co-tenant could read the key.
Impact: With the recovered key an attacker can call the MCP gateway directly over its loopback/host port, bypassing the per-agent --allowed-tools allowlist and invoking any tool the gateway exposes (including safe-outputs / GitHub-mutating tools), defeating a core containment control.
Note: on default single-tenant GitHub-hosted runners the practical exposure is limited because the runner is not shared. The finding is nonetheless actionable because it contradicts the project's own documented protection standard for this exact credential and offers defense-in-depth on shared/self-hosted runners.
Recommendation
- Write the CLI wrapper scripts with owner-only permissions, e.g.
{ mode: 0o700 } instead of 0o755, so the embedded key is not world-readable. The bin directory is already locked to 0o555; keeping the files owner-only closes the read gap.
- Prefer not to embed the API key in the script at all: pass it to the bridge via an environment variable (e.g.
MCP_GATEWAY_API_KEY, which is already available and masked) rather than baking it into a persisted, on-disk --api-key "<key>" argument.
- Apply the same
umask 077 / owner-only pattern already used by convert_gateway_config_copilot.sh and convert_gateway_config_codex.sh for consistency across all files that persist the gateway key.
- Confirm the tools cache file (
mode: 0o644, line 614) does not also contain sensitive material; if it is only tool schemas this is acceptable, but document that intent.
Evidence
Relevant code snippet (mount_mcp_as_cli.cjs)
// Write the CLI wrapper script using the container-accessible URL
const scriptPath = path.join(CLI_BIN_DIR, name);
try {
fs.writeFileSync(scriptPath, generateCLIWrapperScript(name, containerUrl, toolsFile, apiKey, bridgeScript), { mode: 0o755 });
...
}
// generateCLIWrapperScript embeds the key in the script body:
exec node "${safeBridge}" \
--server-name "${safeName}" \
--server-url "${safeUrl}" \
--tools-file "${safeToolsFile}" \
--api-key "${safeApiKey}" \
"$@"
Contrasting hardening for the same credential (convert_gateway_config_copilot.sh)
umask 077
...
chmod 600 "$COPILOT_CONFIG_PATH"
# mcp-config.json contains the bearer token for the MCP gateway; an attacker
# who reads it could bypass the --allowed-tools constraint by issuing raw
# JSON-RPC calls directly to the gateway.
Generated by 🛡️ Daily action/setup/* Security Audit · copilot · opus48 · 107.4 AIC · ⌖ 29.2 AIC · ⊞ 9.3K · ◷
Summary
The MCP CLI wrapper scripts generated by
actions/setup/js/mount_mcp_as_cli.cjsembed the MCP gateway API key in plaintext and are written with world-readable/executable permissions (mode: 0o755) into${RUNNER_TEMP}/gh-aw/mcp-cli/bin. The gateway API key is a security-sensitive credential: the project's own threat model (seeconvert_gateway_config_copilot.sh) states that anyone able to read this key can "bypass the--allowed-toolsconstraint by issuing raw JSON-RPC calls directly to the gateway." Elsewhere in the same action the key is deliberately protected withchmod 600, so this file-permission handling is inconsistent and weakens the intended containment boundary between the agent and the gateway.Affected Files
actions/setup/js/mount_mcp_as_cli.cjsfs.writeFileSync(scriptPath, generateCLIWrapperScript(name, containerUrl, toolsFile, apiKey, bridgeScript), { mode: 0o755 })actions/setup/js/mount_mcp_as_cli.cjs--api-key "${safeApiKey}"in plaintextFor comparison, the intended hardening for the same credential:
actions/setup/sh/convert_gateway_config_copilot.shchmod 600 "$COPILOT_CONFIG_PATH"(+umask 077)actions/setup/sh/convert_gateway_config_codex.shchmod 600 /tmp/gh-aw/mcp-config/config.tomlRisk
Severity: Medium
Attack vector: Any process or user account that can read files under
${RUNNER_TEMP}/gh-aw/mcp-cli/bin(mode0o755files inside a0o555directory — both world-readable) cancata wrapper script and recover the gateway API key. On multi-tenant, self-hosted, or container-shared runners where the workflow does not have exclusive ownership of the runner temp directory, an unprivileged co-tenant could read the key.Impact: With the recovered key an attacker can call the MCP gateway directly over its loopback/host port, bypassing the per-agent
--allowed-toolsallowlist and invoking any tool the gateway exposes (including safe-outputs / GitHub-mutating tools), defeating a core containment control.Note: on default single-tenant GitHub-hosted runners the practical exposure is limited because the runner is not shared. The finding is nonetheless actionable because it contradicts the project's own documented protection standard for this exact credential and offers defense-in-depth on shared/self-hosted runners.
Recommendation
{ mode: 0o700 }instead of0o755, so the embedded key is not world-readable. The bin directory is already locked to0o555; keeping the files owner-only closes the read gap.MCP_GATEWAY_API_KEY, which is already available and masked) rather than baking it into a persisted, on-disk--api-key "<key>"argument.umask 077/ owner-only pattern already used byconvert_gateway_config_copilot.shandconvert_gateway_config_codex.shfor consistency across all files that persist the gateway key.mode: 0o644, line 614) does not also contain sensitive material; if it is only tool schemas this is acceptable, but document that intent.Evidence
Relevant code snippet (mount_mcp_as_cli.cjs)
Contrasting hardening for the same credential (convert_gateway_config_copilot.sh)