Skip to content

Commit 1574c20

Browse files
authored
fix(mcp): pass SSRF-guarded fetch into OAuth start flow, matching probe/revoke (#5398)
Discovery and registration during the MCP OAuth start flow were using the default global fetch. probe.ts and revoke.ts already route these calls through createSsrfGuardedMcpFetch(); this brings the start route in line with the same pattern.
1 parent ff8844c commit 1574c20

2 files changed

Lines changed: 26 additions & 2 deletions

File tree

apps/sim/app/api/mcp/oauth/start/route.test.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@ import {
1717
import { NextRequest } from 'next/server'
1818
import { beforeEach, describe, expect, it, vi } from 'vitest'
1919

20-
const { mockMcpAuth } = vi.hoisted(() => ({
20+
const { mockMcpAuth, mockCreateSsrfGuardedMcpFetch, mockGuardedFetch } = vi.hoisted(() => ({
2121
mockMcpAuth: vi.fn(),
22+
mockCreateSsrfGuardedMcpFetch: vi.fn(),
23+
mockGuardedFetch: vi.fn(),
2224
}))
2325

2426
vi.mock('@sim/db', () => dbChainMock)
@@ -31,6 +33,9 @@ vi.mock('drizzle-orm', () => ({
3133
vi.mock('@modelcontextprotocol/sdk/client/auth.js', () => ({
3234
auth: mockMcpAuth,
3335
}))
36+
vi.mock('@/lib/mcp/pinned-fetch', () => ({
37+
createSsrfGuardedMcpFetch: mockCreateSsrfGuardedMcpFetch,
38+
}))
3439
vi.mock('@/lib/auth/hybrid', () => hybridAuthMock)
3540
vi.mock('@/lib/workspaces/permissions/utils', () => permissionsMock)
3641
vi.mock('@/lib/mcp/oauth', () => mcpOauthMock)
@@ -73,6 +78,21 @@ describe('MCP OAuth start route', () => {
7378
})
7479
mcpOauthMockFns.mockLoadPreregisteredClient.mockResolvedValue(undefined)
7580
mockMcpAuth.mockRejectedValue(new McpOauthRedirectRequiredMock('https://mcp.exa.ai/authorize'))
81+
mockCreateSsrfGuardedMcpFetch.mockReturnValue(mockGuardedFetch)
82+
})
83+
84+
it('routes OAuth discovery through the SSRF-guarded fetch', async () => {
85+
const request = new NextRequest(
86+
'http://localhost:3000/api/mcp/oauth/start?workspaceId=workspace-1&serverId=server-1'
87+
)
88+
89+
await GET(request)
90+
91+
expect(mockCreateSsrfGuardedMcpFetch).toHaveBeenCalledTimes(1)
92+
expect(mockMcpAuth).toHaveBeenCalledWith(
93+
expect.anything(),
94+
expect.objectContaining({ serverUrl: 'https://mcp.exa.ai/mcp', fetchFn: mockGuardedFetch })
95+
)
7696
})
7797

7898
it('requires workspace write permission via MCP auth middleware', async () => {

apps/sim/app/api/mcp/oauth/start/route.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
SimMcpOauthProvider,
2121
setOauthRowUser,
2222
} from '@/lib/mcp/oauth'
23+
import { createSsrfGuardedMcpFetch } from '@/lib/mcp/pinned-fetch'
2324
import { createMcpErrorResponse } from '@/lib/mcp/utils'
2425

2526
const logger = createLogger('McpOauthStartAPI')
@@ -129,7 +130,10 @@ export const GET = withRouteHandler(
129130
const provider = new SimMcpOauthProvider({ row, preregistered })
130131

131132
try {
132-
const result = await mcpAuth(provider, { serverUrl: server.url })
133+
const result = await mcpAuth(provider, {
134+
serverUrl: server.url,
135+
fetchFn: createSsrfGuardedMcpFetch(),
136+
})
133137
if (result === 'AUTHORIZED') {
134138
return NextResponse.json({ status: 'already_authorized' })
135139
}

0 commit comments

Comments
 (0)