Skip to content

An MCP token can be spent against any address, by re-addressing its server or by orphaning it and creating the server again #237

Description

@beardthelion

Attaching a credential to an MCP server is the one place this deployment accepts a reference to a stored secret rather than the secret itself. Everywhere else the value arrives in the request that stores it, and the id it gets is nobody's to choose: storeAgentAuth mints its own row from the key an administrator typed. The add is also what spends the credential, because refreshTools runs before the call returns and sends what it decrypts to the URL from that same request.

Three ways the credential and the address can be made to disagree. All three were run against a real database with fetch captured, and all three delivered a stored token to a host named in the request.

1. Any mcp credential can be attached to any server. provider records the server a token was minted for, and nothing reads it. Naming another vendor's token while adding a server at an address of your choosing is accepted, and the outbound request carries that token.

2. Re-adding a server rewrites its address and keeps its credential. addCustomServer is an upsert that sets url: input.url on conflict. So the token really does belong to the server, and only the address moved. This needs no cross-server trick at all, and a check on ownership cannot see it.

3. A token can be orphaned and then re-attached anywhere. This is the one worth reading twice, because each step is an ordinary administrative act:

  • re-add the server naming no credential. The upsert sets credential_id to null while the credential stays live, pointing at nothing.
  • remove the server. removeServer retires a token by reading it off that pointer, so with the pointer cleared it revokes nothing and deletes the row. The token now outlives the server it was minted for.
  • create the server again at any address, naming that orphaned credential. There is no stored address left to compare against, so it is accepted and the token goes out.

Observed, before any fix:

1 establish at the legitimate address: ACCEPTED   sent: legit.vendor.example
2 re-add with no token:                ACCEPTED   pointer survived: false
3 after removing the server, token still live: true
4 re-create at a chosen address naming it: ACCEPTED   sent: collector.attacker.example [LEAKED]

Why this is more than an administrator misconfiguring something. A stored credential cannot be read back by design. GET /api/admin/credentials answers with id, kind, provider, keyId, metadata and revoked-at, and never a value, and storeMcpToken says in its own comment that the token goes one way. These three shapes are the way around that: they hand an administrator the plaintext of a credential somebody else configured, before any Bot, grant or policy check exists. A deployment where any of them has been used should treat the credentials involved as disclosed and rotate them.

Concurrency is not part of it: two concurrent re-adds at different addresses were also run, and the second is refused.

Curated servers are not affected by 2 or 3. Their URL comes from the catalogue rather than the request, and a per-instance hostname is checked against the vendor's anchored pattern before anything is stored.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions