Skip to content

Security

Chenglei Yuan edited this page Jun 4, 2026 · 1 revision

Security

This page describes the proxy's security model and the defenses built into it. For the code that implements these checks, see Architecture, Bridge Internals, and OAuth2 Internals.

Identity and token model

  • The proxy holds OAuth2 credentials and acquires tokens on behalf of the configured client. It does not perform per-user delegation beyond whatever the single configured grant represents.
  • Tokens live in memory in a single TokenManager. Only the refresh token (for authorization_code) is persisted, and only encrypted.

Authorization-header handling

The proxy is the sole authority for the upstream Authorization header. It sets Authorization: Bearer <token> on every upstream request itself; the MCP client never needs to — and cannot usefully — inject one through the stdio channel.

Logging is stderr-only and redacted

  • stdout is reserved exclusively for JSON-RPC. Anything else on stdout would corrupt the MCP stream, so all diagnostics go to stderr via pino.
  • Logs redact secrets: access_token, refresh_token, client_secret, Authorization headers, and the oauth2.clientSecret, oauth2.refreshToken, oauth2.authorizationCode, and oauth2.codeVerifier config fields are all replaced with [redacted].

Transport security

Cleartext HTTP is rejected unless it is provably safe:

  • https:// URLs always pass.
  • http:// URLs pass only if the host is loopback (localhost, 127.0.0.0/8, ::1) — useful for local dev and SSH tunnels — or if you have explicitly set allowInsecureHttp: true (ALLOW_INSECURE_HTTP=true).
  • Any other cleartext URL throws at startup.

This check (assertSecureUrl) runs at load time against upstream.url, oauth2.tokenUrl, and oauth2.authorizationUrl. Discovered endpoints get the same treatment via keepSecure (insecure ones are dropped — see Discovery).

Enable allowInsecureHttp only for trusted local testing. In production, always use https:// end to end.

Callback listener hardening (interactive flow)

The local OAuth callback server used by the authorization_code flow defends against two classes of attack:

  • DNS rebinding. The listener rejects any request whose Host header is not a loopback address or whose port doesn't match the bound port. A remote page that resolves a hostname to 127.0.0.1 therefore can't drive the callback.
  • CSRF. The OAuth state parameter is generated randomly and validated on the redirect; a mismatch is rejected.
  • It warns if you bind the callback to a non-loopback host (callbackHost), since that widens exposure.

PKCE (S256) is always used, so an intercepted authorization code is useless without the verifier.

Refresh-token cache at rest

  • Encrypted with AES-256-GCM; the key is a random 32-byte file stored with 0600 permissions.
  • Stored under the per-user OS config directory (see OAuth2 Grants and Tokens).
  • Caveat: this protects against casual disk/backup exposure, not against an attacker who already has read access to your user account — the key sits beside the ciphertext by design (so launches stay non-interactive). Treat the config directory like any other credential store: rely on OS file permissions and full-disk encryption.

Operational guidance

  • Prefer environment variables for secrets so they don't persist in a config file.
  • Keep the proxy and the upstream on https:// (or a loopback SSH tunnel) at all times.
  • Set upstream.timeoutMs sensibly to bound how long a hung upstream can tie up a request.

Clone this wiki locally