fix private mcp server dns resolution#43
Conversation
|
Automated review 🤖 Summary of Changes This PR delivers two independent fixes: (1) a SSRF bypass allowlist for private-IP MCP hostnames via Architecture & Organization
Key Changes & Positives
Potential Issues & Recommendations
Security & Privacy
Tests
Approval Recommendation Approve with caveats
|
There was a problem hiding this comment.
Pull request overview
This PR updates external MCP SSRF validation to support explicitly allowlisting hostnames that resolve to private IPs (to unblock private MCP deployments), and improves OAuth refresh error handling for the google-workspace integration.
Changes:
- Add
EXTERNAL_MCP_PRIVATE_HOST_ALLOWLIST-driven bypass for private-IP DNS resolution checks invalidateExternalMcpUrl. - Map
unsupported_grant_typerefresh failures toreauth_requiredforgoogle-workspace. - Add Jest coverage for the new SSRF allowlist behavior and the new OAuth refresh mapping; bump package version.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| test/mcp-external-auth.spec.ts | Adds tests for private-IP DNS allowlisting and google-workspace refresh error mapping. |
| src/utils/ssrf.ts | Implements hostname allowlist for private-IP DNS resolutions in external MCP URL validation. |
| src/services/oauthTokens.ts | Treats unsupported_grant_type refresh failures as reauth-required for google-workspace. |
| package.json | Bumps package version to 1.11.8. |
Comments suppressed due to low confidence (1)
test/mcp-external-auth.spec.ts:221
- This test mutates process.env.EXTERNAL_MCP_PRIVATE_HOST_ALLOWLIST and restores it only after the assertion. If the expectation fails, the env var and dns.lookup spy may not be restored, causing cross-test pollution. Wrap the body in try/finally (restore env + mockRestore) or move restoration into afterEach.
const previousAllowlist = process.env.EXTERNAL_MCP_PRIVATE_HOST_ALLOWLIST
process.env.EXTERNAL_MCP_PRIVATE_HOST_ALLOWLIST = 'googlemcp.missionsquad.ai'
const lookupSpy = jest.spyOn(dns, 'lookup').mockImplementation(async () =>
[{ address: '10.1.253.40', family: 4 }] as any
)
await expect(validateExternalMcpUrl('https://googlemcp.missionsquad.ai/mcp')).resolves.toMatchObject({
hostname: 'googlemcp.missionsquad.ai',
pathname: '/mcp'
})
if (previousAllowlist === undefined) {
delete process.env.EXTERNAL_MCP_PRIVATE_HOST_ALLOWLIST
} else {
process.env.EXTERNAL_MCP_PRIVATE_HOST_ALLOWLIST = previousAllowlist
}
lookupSpy.mockRestore()
})
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const trustedPrivateHostnameAllowlist = getTrustedPrivateHostnameAllowlist() | ||
| const allowlistedHostname = isHostnameAllowlisted(hostname, trustedPrivateHostnameAllowlist) | ||
| const resolved = await dns.lookup(hostname, { all: true, verbatim: true }) | ||
| if (resolved.length === 0) { | ||
| throw new McpValidationError(`Unable to resolve external MCP hostname: ${hostname}`) | ||
| } | ||
|
|
||
| resolved.forEach(({ address }) => assertSafeIp(address)) | ||
| if (!allowlistedHostname) { | ||
| resolved.forEach(({ address }) => assertSafeIp(address)) | ||
| } |
| const lookupSpy = jest.spyOn(dns, 'lookup').mockImplementation(async () => | ||
| [{ address: '10.1.253.40', family: 4 }] as any | ||
| ) | ||
|
|
||
| await expect(validateExternalMcpUrl('https://googlemcp.missionsquad.ai/mcp')).rejects.toThrow( | ||
| 'Blocked private or local IPv4 address: 10.1.253.40' | ||
| ) | ||
|
|
||
| lookupSpy.mockRestore() | ||
| }) |
No description provided.