Summary
In the github-readonly environment (Claude engine, claude-sonnet-4-6), the MCP gateway bearer token stored in plaintext at /tmp/gh-aw/mcp-config/mcp-servers.json is shared between the GitHub read MCP server and the safeoutputs write-sink MCP server. A subprocess with runner-user access can read this token, initialize an authenticated safeoutputs MCP session, and call create_issue — a write operation — from within a job that declares only contents: read, actions: read, and issues: read. No GITHUB_TOKEN is present in this environment, making the safeoutputs channel the only write path and the shared bearer token the sole access control. The declared issues: read permission ceiling is therefore contradicted. This is the same shared-token root cause as #23740 (github-edit / Copilot engine), but confirmed for the github-readonly / Claude engine profile.
Affected Area
Safe-outputs write-sink caller-identity boundary — github-readonly environment class, Claude engine. The LD_PRELOAD one-shot token library protects environment-variable tokens but does not intercept file-based MCP gateway credentials in /tmp/gh-aw/mcp-config/mcp-servers.json.
Reproduction Outline
- Configure a workflow with the
github-readonly profile, Claude engine, and safe-outputs: create-issue. Declare permissions contents: read, actions: read, issues: read (no write). Give the agent Bash tool access.
- During a run, read the shared token from
/tmp/gh-aw/mcp-config/mcp-servers.json — both the github and safeoutputs server entries carry the same Authorization header value.
- Use
curl with that token to POST an MCP initialize request to the safeoutputs server URL (host.docker.internal/...). Observe HTTP 200 and a valid Mcp-Session-Id.
- Call
tools/list on the established session. Observe create_issue is present and callable.
- Call
create_issue via the safeoutputs MCP session from bash. The operation is enqueued to $GH_AW_SAFE_OUTPUTS (the JSONL queue), which the downstream safeoutputs job (holding issues: write) will execute.
Observed Behavior
Safeoutputs MCP session initializes successfully from bash (HTTP 200); create_issue is available and can enqueue a write operation despite the job declaring only issues: read. The declared permission ceiling does not constrain the safeoutputs channel.
Expected Behavior
The safeoutputs write-sink should reject callers that are not the authorized agent process. A bash subprocess reading the shared file-based token should receive HTTP 403 or a JSON-RPC error when attempting to initialize a safeoutputs session or call a write tool.
Security Relevance
Any bash command or prompt-injected subprocess running as the runner user can silently escalate from declared read permissions to effective write (issue creation, and potentially other safe-outputs operations). In the github-readonly profile, where there is no GITHUB_TOKEN and the safeoutputs channel is the only write path, this completely undermines the permission ceiling guarantee. The LD_PRELOAD one-shot library does not cover file-based credentials, leaving this bypass unmitigated by the existing token-protection mechanism.
Suggested mitigations (from the original finding):
- Issue separate, scoped bearer tokens per MCP server (read vs. write-sink)
- Enforce caller-identity on the safeoutputs gateway (process-bound secret, Unix socket, or session token not stored in a file)
- Extend the one-shot token protection to cover file-based MCP gateway credentials
gh-aw version: v0.65.6
Original finding: https://github.com/githubnext/gh-aw-security/issues/1701
Generated by File Issue · ● 303.3K · ◷
Summary
In the
github-readonlyenvironment (Claude engine,claude-sonnet-4-6), the MCP gateway bearer token stored in plaintext at/tmp/gh-aw/mcp-config/mcp-servers.jsonis shared between the GitHub read MCP server and the safeoutputs write-sink MCP server. A subprocess with runner-user access can read this token, initialize an authenticated safeoutputs MCP session, and callcreate_issue— a write operation — from within a job that declares onlycontents: read,actions: read, andissues: read. NoGITHUB_TOKENis present in this environment, making the safeoutputs channel the only write path and the shared bearer token the sole access control. The declaredissues: readpermission ceiling is therefore contradicted. This is the same shared-token root cause as #23740 (github-edit/ Copilot engine), but confirmed for thegithub-readonly/ Claude engine profile.Affected Area
Safe-outputs write-sink caller-identity boundary —
github-readonlyenvironment class, Claude engine. TheLD_PRELOADone-shot token library protects environment-variable tokens but does not intercept file-based MCP gateway credentials in/tmp/gh-aw/mcp-config/mcp-servers.json.Reproduction Outline
github-readonlyprofile, Claude engine, andsafe-outputs: create-issue. Declare permissionscontents: read,actions: read,issues: read(no write). Give the agentBashtool access./tmp/gh-aw/mcp-config/mcp-servers.json— both thegithubandsafeoutputsserver entries carry the sameAuthorizationheader value.curlwith that token to POST an MCPinitializerequest to the safeoutputs server URL (host.docker.internal/...). Observe HTTP 200 and a validMcp-Session-Id.tools/liston the established session. Observecreate_issueis present and callable.create_issuevia the safeoutputs MCP session from bash. The operation is enqueued to$GH_AW_SAFE_OUTPUTS(the JSONL queue), which the downstream safeoutputs job (holdingissues: write) will execute.Observed Behavior
Safeoutputs MCP session initializes successfully from bash (HTTP 200);
create_issueis available and can enqueue a write operation despite the job declaring onlyissues: read. The declared permission ceiling does not constrain the safeoutputs channel.Expected Behavior
The safeoutputs write-sink should reject callers that are not the authorized agent process. A bash subprocess reading the shared file-based token should receive HTTP 403 or a JSON-RPC error when attempting to initialize a safeoutputs session or call a write tool.
Security Relevance
Any bash command or prompt-injected subprocess running as the runner user can silently escalate from declared
readpermissions to effectivewrite(issue creation, and potentially other safe-outputs operations). In thegithub-readonlyprofile, where there is noGITHUB_TOKENand the safeoutputs channel is the only write path, this completely undermines the permission ceiling guarantee. TheLD_PRELOADone-shot library does not cover file-based credentials, leaving this bypass unmitigated by the existing token-protection mechanism.Suggested mitigations (from the original finding):
gh-aw version: v0.65.6
Original finding: https://github.com/githubnext/gh-aw-security/issues/1701