Skip to content

fix(mcp): correct stale SSRF pin docstring and bound the remaining OAuth callback steps#5845

Closed
waleedlatif1 wants to merge 1 commit into
stagingfrom
fix/mcp-audit-followups
Closed

fix(mcp): correct stale SSRF pin docstring and bound the remaining OAuth callback steps#5845
waleedlatif1 wants to merge 1 commit into
stagingfrom
fix/mcp-audit-followups

Conversation

@waleedlatif1

Copy link
Copy Markdown
Collaborator

Summary

Two follow-ups from a full MCP lifecycle audit — both on merged code, both low-risk:

  • Stale SSRF docstring (security-relevant). validateMcpServerSsrf's return-value docstring still said it returns an IP to "pin subsequent connections to ... stop redirects from escaping." That's no longer true for the public path (post validate-at-connect): the returned IP is a policy signal selecting the guarded fetch, and redirect/rebind safety comes from per-connect validation + followRedirectsGuarded, not pinning. Only the self-hosted private carve-out still literally pins. Corrected so a future reader can't reintroduce a real pin from the misleading text. Doc-only, no behavior change.
  • OAuth callback timeout consistency. The callback bounded only the five post-auth steps in timedStep; loadOauthRowByState, getSession, the server SELECT, and the provider_error clearState were unbounded — contradicting the "every awaited step is bounded" invariant. Now a wedged DB read on those surfaces as a labeled timeout instead of hanging (the client 10-min backstop already prevented a stranded tab; this closes the server-side consistency gap).

Type of Change

  • Bug fix (robustness + doc correctness)

Testing

  • 91 tests green across domain-check + callback-route suites; tsc + biome clean.

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

…uth callback steps

Lifecycle-audit follow-ups (merged-code):
- validateMcpServerSsrf's return docstring described 'pin subsequent connections',
  which no longer holds for the public path — the returned IP is a policy signal
  selecting the validate-at-connect guarded fetch; redirect/rebind safety comes
  from per-connect validation + followRedirectsGuarded, not pinning (only the
  self-hosted private carve-out still literally pins). Corrected so a future
  reader can't reintroduce a real pin from the misleading text.
- The callback wrapped only the five post-auth steps in timedStep; the earlier
  loadOauthRowByState, getSession, and server SELECT (plus the provider_error
  clearState) were unbounded, contradicting the 'every awaited step bounded'
  invariant. Wrapped them so a wedged DB read surfaces as a labeled timeout.
@vercel

vercel Bot commented Jul 22, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Jul 22, 2026 7:00am

Request Review

@cursor

cursor Bot commented Jul 22, 2026

Copy link
Copy Markdown

PR Summary

Low Risk
Small robustness and documentation-only SSRF clarification; OAuth callback timeouts only change failure mode on stalls, not auth or token exchange logic.

Overview
OAuth callback now wraps the previously unbounded awaits—initial OAuth row load, session lookup, MCP server SELECT, and provider-error clearState—in timedStep (10–15s) so wedged DB/session work fails with labeled timeouts instead of hanging the route.

SSRF docs on validateMcpServerSsrf are updated to match current behavior: a non-null return is a policy signal that enables validate-at-connect guarded fetch (rebind/redirect safety via per-connect validation and followRedirectsGuarded), not blanket connection pinning—only the self-hosted private/loopback carve-out still pins literally. No runtime change to SSRF logic.

Reviewed by Cursor Bugbot for commit d4eb889. Configure here.

@greptile-apps

greptile-apps Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR bounds more OAuth callback operations and updates the SSRF validator documentation. The main changes are:

  • Adds timeouts to OAuth state, session, server, and cleanup operations.
  • Updates the SSRF return-value documentation to describe guarded public connections.
  • Keeps the self-hosted private-address pinning carve-out documented.

Confidence Score: 3/5

The OAuth timeout paths can report the wrong failure and can return before state cleanup completes.

  • A slow state lookup is reported as an invalid OAuth state.
  • Provider-error cleanup can continue after the response while the state remains reusable.
  • A session timeout loses the initiating server ID in the failure broadcast.
  • The SSRF documentation still conflicts with the OAuth probe behavior.

apps/sim/app/api/mcp/oauth/callback/route.ts and apps/sim/lib/mcp/domain-check.ts

Security Review

The provider-error timeout can return before OAuth state cleanup completes, leaving the state temporarily reusable. The revised SSRF documentation also omits that the OAuth probe still pins public resolved addresses.

Important Files Changed

Filename Overview
apps/sim/app/api/mcp/oauth/callback/route.ts Adds callback timeouts, but misclassifies state lookup failures and can return before state cleanup completes.
apps/sim/lib/mcp/domain-check.ts Updates SSRF documentation, but the policy-signal description does not cover the pinned OAuth probe.

Reviews (1): Last reviewed commit: "fix(mcp): correct stale SSRF pin docstri..." | Re-trigger Greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

Consolidated into #5842 — the SSRF docstring correction and OAuth callback timedStep bounding are now part of that single PR alongside the push-subscription and lifecycle-audit fixes. Closing to keep everything in one clean PR.

@waleedlatif1
waleedlatif1 deleted the fix/mcp-audit-followups branch July 22, 2026 07:02
Comment on lines +128 to +131
const initialRow = state
? await timedStep('loadOauthRowByState', 15_000, () => loadOauthRowByState(state)).catch(
() => null
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Timeout Becomes Invalid State

When a valid state lookup exceeds 15 seconds or the database rejects it, this catch converts the failure to null. The callback then reports invalid_state, so the client treats a transient infrastructure failure as an expired or forged OAuth state instead of receiving the labeled timeout this change intends to provide.

Comment on lines +138 to +140
await timedStep('clearState(provider_error)', 10_000, () =>
clearState(initialRow.id, 'callback:provider_error')
).catch(() => {})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security State Cleanup Outlives Callback

When this update exceeds 10 seconds, timedStep rejects without cancelling the database operation and the catch lets the callback return immediately. The OAuth state therefore remains valid until the background update eventually completes, so another callback using the same state can be accepted after the provider-error response has already been sent.

let serverId: string | undefined
try {
const session = await getSession()
const session = await timedStep('getSession', 15_000, () => getSession())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Session Timeout Drops Server Identity

If getSession reaches the new timeout, control jumps to the callback catch before serverId is assigned from the already loaded OAuth row. The failure broadcast therefore contains no server ID, preventing the opener from associating the failed authorization with the server that initiated it.

Comment on lines +144 to +148
* resolution selects the validate-at-connect guarded fetch — DNS-rebinding TOCTOU
* and redirect escapes are prevented by re-validating every socket connect and
* following redirects under per-hop validation (see `createSsrfGuardedMcpFetch` /
* `followRedirectsGuarded`), NOT by pinning to this address. The value is literally
* pinned only for the self-hosted private/loopback carve-out (a policy-permitted

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 security Public Probe Still Pins Address

The new text says a public result is not pinned, but the OAuth probe passes every non-null resolvedIP, including public addresses, to createPinnedFetchWithDispatcher. The main transport uses the value as a policy signal, while the probe still pins it, so this security guidance remains inaccurate for an existing caller.

Suggested change
* resolution selects the validate-at-connect guarded fetch DNS-rebinding TOCTOU
* and redirect escapes are prevented by re-validating every socket connect and
* following redirects under per-hop validation (see `createSsrfGuardedMcpFetch` /
* `followRedirectsGuarded`), NOT by pinning to this address. The value is literally
* pinned only for the self-hosted private/loopback carve-out (a policy-permitted
* resolution selects the validate-at-connect guarded fetch for the main transport
* DNS-rebinding TOCTOU and redirect escapes are prevented by re-validating every
* socket connect and following redirects under per-hop validation (see
* `createSsrfGuardedMcpFetch` / `followRedirectsGuarded`). Some one-shot probes still
* pin this address, as does the self-hosted private/loopback carve-out (a policy-permitted

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.

1 participant