Skip to content

Safeoutputs write-sink reachable from bash in github-readonly profile — shared MCP bearer token bypasses declared read-only perm [Content truncated due to length] #26584

@szabta89

Description

@szabta89

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

  1. 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.
  2. 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.
  3. 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.
  4. Call tools/list on the established session. Observe create_issue is present and callable.
  5. 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 ·

Metadata

Metadata

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions