From 2ad06cb8be98444ff8dc19e97d510d260641e15c Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 20 May 2026 12:26:22 -0700 Subject: [PATCH] feat(security): add ALLOWED_PRIVATE_HOSTS allowlist for SSRF block MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Self-hosted operators frequently need agents, webhooks, database blocks, and MCP servers to reach internal services (on-prem GitLab, internal SIEM, in-cluster Postgres) whose hostnames resolve to private IPs. Today the SSRF block is binary — only ALLOWED_MCP_DOMAINS provides an escape, and only for MCP. ALLOWED_PRIVATE_HOSTS accepts a comma-separated list of hostnames, literal IPs, and CIDRs. Entries are matched against both the original hostname and the resolved IP, so "gitlab.internal" or "10.112.12.56" or "10.0.0.0/8" all work. The default (unset) preserves today's full private-IP block. Loopback handling and the hosted-mode tightening are unchanged — the allowlist only narrows the private/reserved range check. Co-Authored-By: Claude Opus 4.7 --- .../en/self-hosting/environment-variables.mdx | 2 + .../docs/en/self-hosting/troubleshooting.mdx | 15 ++ apps/sim/lib/core/config/env.ts | 1 + .../sim/lib/core/config/feature-flags.test.ts | 134 ++++++++++++++++++ apps/sim/lib/core/config/feature-flags.ts | 102 +++++++++++++ .../core/security/input-validation.server.ts | 19 ++- .../sim/lib/core/security/input-validation.ts | 8 +- apps/sim/lib/mcp/domain-check.test.ts | 65 ++++++++- apps/sim/lib/mcp/domain-check.ts | 13 +- helm/sim/values.yaml | 1 + .../testing/src/mocks/feature-flags.mock.ts | 3 + 11 files changed, 348 insertions(+), 15 deletions(-) create mode 100644 apps/sim/lib/core/config/feature-flags.test.ts diff --git a/apps/docs/content/docs/en/self-hosting/environment-variables.mdx b/apps/docs/content/docs/en/self-hosting/environment-variables.mdx index 1a048777552..5f8b92b777d 100644 --- a/apps/docs/content/docs/en/self-hosting/environment-variables.mdx +++ b/apps/docs/content/docs/en/self-hosting/environment-variables.mdx @@ -69,6 +69,8 @@ import { Callout } from 'fumadocs-ui/components/callout' | `RESEND_API_KEY` | Email service for notifications | | `ALLOWED_LOGIN_DOMAINS` | Restrict signups to domains (comma-separated) | | `ALLOWED_LOGIN_EMAILS` | Restrict signups to specific emails (comma-separated) | +| `ALLOWED_MCP_DOMAINS` | Restrict outbound MCP servers to listed domains (comma-separated). Empty = all allowed | +| `ALLOWED_PRIVATE_HOSTS` | Allowlist of hostnames, IPs, or CIDRs exempt from SSRF private-IP blocking (e.g., `gitlab.internal,10.0.0.0/8`). Use to call internal services from HTTP, webhook, database, or MCP blocks | | `DISABLE_REGISTRATION` | Set to `true` to disable new user signups | ## Example .env diff --git a/apps/docs/content/docs/en/self-hosting/troubleshooting.mdx b/apps/docs/content/docs/en/self-hosting/troubleshooting.mdx index 63cc71491fb..c83766d51ad 100644 --- a/apps/docs/content/docs/en/self-hosting/troubleshooting.mdx +++ b/apps/docs/content/docs/en/self-hosting/troubleshooting.mdx @@ -58,6 +58,21 @@ Use the correct PostgreSQL image: image: pgvector/pgvector:pg17 # NOT postgres:17 ``` +## "URL resolves to a blocked IP address" + +Sim refuses outbound calls to private/reserved IP ranges (RFC-1918, link-local, cloud metadata) as SSRF protection. If you need agents, webhooks, or database blocks to reach an internal service (e.g., on-prem GitLab, internal SIEM, in-cluster Postgres), allowlist it: + +```bash +# Hostnames, literal IPs, or CIDRs (comma-separated) +ALLOWED_PRIVATE_HOSTS=gitlab.internal,10.112.12.56,10.0.0.0/8 +``` + +- Hostnames are matched case-insensitively against the original URL hostname. +- IPs and CIDRs are matched against the resolved IP after DNS lookup, so `gitlab.internal` resolving to `10.x.x.x` will be allowed if either the hostname or the IP is listed. +- Restart the app after changing the value. + +For MCP servers specifically, [`ALLOWED_MCP_DOMAINS`](/mcp#domain-allowlisting) is the preferred control — setting it disables the default private-IP block for MCP entirely, replacing it with the curated domain allowlist. + ## Certificate Errors (CERT_HAS_EXPIRED) If you see SSL certificate errors when calling external APIs: diff --git a/apps/sim/lib/core/config/env.ts b/apps/sim/lib/core/config/env.ts index 63070896dd2..83ff15890dc 100644 --- a/apps/sim/lib/core/config/env.ts +++ b/apps/sim/lib/core/config/env.ts @@ -132,6 +132,7 @@ export const env = createEnv({ BLACKLISTED_PROVIDERS: z.string().optional(), // Comma-separated provider IDs to hide (e.g., "openai,anthropic") BLACKLISTED_MODELS: z.string().optional(), // Comma-separated model names/prefixes to hide (e.g., "gpt-4,claude-*") ALLOWED_MCP_DOMAINS: z.string().optional(), // Comma-separated domains for MCP servers (e.g., "internal.company.com,mcp.example.org"). Empty = all allowed. + ALLOWED_PRIVATE_HOSTS: z.string().optional(), // Comma-separated hostnames, IPs, or CIDRs to exempt from SSRF private-IP blocking (e.g., "gitlab.allot.internal,10.112.12.56,10.0.0.0/8"). Empty = SSRF block enforced for all private/reserved IPs. ALLOWED_INTEGRATIONS: z.string().optional(), // Comma-separated block types to allow (e.g., "slack,github,agent"). Empty = all allowed. // Azure Configuration - Shared credentials with feature-specific models diff --git a/apps/sim/lib/core/config/feature-flags.test.ts b/apps/sim/lib/core/config/feature-flags.test.ts new file mode 100644 index 00000000000..d82f937da93 --- /dev/null +++ b/apps/sim/lib/core/config/feature-flags.test.ts @@ -0,0 +1,134 @@ +/** + * @vitest-environment node + */ +import { createEnvMock } from '@sim/testing' +import { afterEach, describe, expect, it, vi } from 'vitest' + +vi.mock('@/lib/core/config/env', () => createEnvMock()) + +import { env } from '@/lib/core/config/env' +import { + __resetAllowedPrivateHostsCacheForTest, + getAllowedPrivateHostsFromEnv, + isAllowlistedPrivateHost, +} from '@/lib/core/config/feature-flags' + +function withAllowedPrivateHosts(value: string | undefined) { + ;(env as { ALLOWED_PRIVATE_HOSTS?: string }).ALLOWED_PRIVATE_HOSTS = value + __resetAllowedPrivateHostsCacheForTest() +} + +describe('getAllowedPrivateHostsFromEnv', () => { + afterEach(() => { + withAllowedPrivateHosts(undefined) + }) + + it('returns null when env var is unset', () => { + expect(getAllowedPrivateHostsFromEnv()).toBeNull() + }) + + it('returns null when env var is empty after trimming', () => { + withAllowedPrivateHosts(' , , ') + expect(getAllowedPrivateHostsFromEnv()).toBeNull() + }) + + it('parses bare hostnames into the hostname set (lowercased)', () => { + withAllowedPrivateHosts('Gitlab.Allot.Internal,siem.allot.internal') + const result = getAllowedPrivateHostsFromEnv() + expect(result?.hostnames).toEqual(new Set(['gitlab.allot.internal', 'siem.allot.internal'])) + expect(result?.cidrs).toEqual([]) + }) + + it('parses literal IPs as exact /32 or /128 CIDRs', () => { + withAllowedPrivateHosts('10.112.12.56,fd00::1') + const result = getAllowedPrivateHostsFromEnv() + expect(result?.cidrs).toHaveLength(2) + expect(result?.cidrs[0][1]).toBe(32) + expect(result?.cidrs[1][1]).toBe(128) + }) + + it('parses CIDR ranges', () => { + withAllowedPrivateHosts('10.0.0.0/8,fd00::/8') + const result = getAllowedPrivateHostsFromEnv() + expect(result?.cidrs).toHaveLength(2) + expect(result?.cidrs[0][1]).toBe(8) + expect(result?.cidrs[1][1]).toBe(8) + }) + + it('mixes hostnames, IPs, and CIDRs in one list', () => { + withAllowedPrivateHosts('gitlab.internal, 10.0.0.0/8 ,10.112.12.56 ') + const result = getAllowedPrivateHostsFromEnv() + expect(result?.hostnames.has('gitlab.internal')).toBe(true) + expect(result?.cidrs).toHaveLength(2) + }) + + it('falls back to hostname when CIDR parse fails', () => { + withAllowedPrivateHosts('not-a-cidr/bogus') + const result = getAllowedPrivateHostsFromEnv() + expect(result?.hostnames.has('not-a-cidr/bogus')).toBe(true) + }) + + it('caches the parse result across calls', () => { + withAllowedPrivateHosts('gitlab.internal') + const first = getAllowedPrivateHostsFromEnv() + ;(env as { ALLOWED_PRIVATE_HOSTS?: string }).ALLOWED_PRIVATE_HOSTS = 'changed.internal' + expect(getAllowedPrivateHostsFromEnv()).toBe(first) + }) +}) + +describe('isAllowlistedPrivateHost', () => { + afterEach(() => { + withAllowedPrivateHosts(undefined) + }) + + it('returns false when env var is unset', () => { + expect(isAllowlistedPrivateHost({ ip: '10.0.0.1' })).toBe(false) + expect(isAllowlistedPrivateHost({ hostname: 'gitlab.internal' })).toBe(false) + }) + + it('matches hostnames case-insensitively', () => { + withAllowedPrivateHosts('gitlab.allot.internal') + expect(isAllowlistedPrivateHost({ hostname: 'GITLAB.ALLOT.INTERNAL' })).toBe(true) + expect(isAllowlistedPrivateHost({ hostname: 'other.internal' })).toBe(false) + }) + + it('matches literal IPv4 entries', () => { + withAllowedPrivateHosts('10.112.12.56') + expect(isAllowlistedPrivateHost({ ip: '10.112.12.56' })).toBe(true) + expect(isAllowlistedPrivateHost({ ip: '10.112.12.57' })).toBe(false) + }) + + it('matches IPv4 CIDR ranges', () => { + withAllowedPrivateHosts('10.0.0.0/8') + expect(isAllowlistedPrivateHost({ ip: '10.0.0.1' })).toBe(true) + expect(isAllowlistedPrivateHost({ ip: '10.255.255.255' })).toBe(true) + expect(isAllowlistedPrivateHost({ ip: '11.0.0.1' })).toBe(false) + expect(isAllowlistedPrivateHost({ ip: '192.168.1.1' })).toBe(false) + }) + + it('matches IPv6 CIDR ranges', () => { + withAllowedPrivateHosts('fc00::/7') + expect(isAllowlistedPrivateHost({ ip: 'fd00::1' })).toBe(true) + expect(isAllowlistedPrivateHost({ ip: 'fd12:3456::1' })).toBe(true) + expect(isAllowlistedPrivateHost({ ip: 'fc00::1' })).toBe(true) + expect(isAllowlistedPrivateHost({ ip: '2001:db8::1' })).toBe(false) + }) + + it('does not cross-match IPv4 and IPv6 ranges', () => { + withAllowedPrivateHosts('10.0.0.0/8') + expect(isAllowlistedPrivateHost({ ip: 'fd00::1' })).toBe(false) + }) + + it('returns true if either hostname or IP matches', () => { + withAllowedPrivateHosts('gitlab.allot.internal,10.0.0.0/8') + expect(isAllowlistedPrivateHost({ hostname: 'gitlab.allot.internal', ip: '8.8.8.8' })).toBe( + true + ) + expect(isAllowlistedPrivateHost({ hostname: 'other.internal', ip: '10.5.5.5' })).toBe(true) + }) + + it('returns false for unparseable IPs', () => { + withAllowedPrivateHosts('10.0.0.0/8') + expect(isAllowlistedPrivateHost({ ip: 'not-an-ip' })).toBe(false) + }) +}) diff --git a/apps/sim/lib/core/config/feature-flags.ts b/apps/sim/lib/core/config/feature-flags.ts index f1d6e959f36..5cef35a91bd 100644 --- a/apps/sim/lib/core/config/feature-flags.ts +++ b/apps/sim/lib/core/config/feature-flags.ts @@ -1,6 +1,7 @@ /** * Environment utility functions for consistent environment detection across the application */ +import * as ipaddr from 'ipaddr.js' import { env, getEnv, isFalsy, isTruthy } from './env' /** @@ -267,6 +268,107 @@ export function getAllowedMcpDomainsFromEnv(): string[] | null { return parsed.length > 0 ? parsed : null } +/** + * Parsed form of the ALLOWED_PRIVATE_HOSTS env var. + * - `hostnames`: lowercase hostnames matched against the original URL hostname + * - `cidrs`: parsed IP ranges matched against the resolved IP after DNS lookup + */ +export interface AllowedPrivateHosts { + hostnames: Set + cidrs: Array<[ipaddr.IPv4 | ipaddr.IPv6, number]> +} + +let cachedAllowedPrivateHosts: AllowedPrivateHosts | null | undefined + +/** + * Get the parsed allowlist of private hosts and CIDRs that should bypass SSRF + * private-IP blocking. + * + * Returns null if `ALLOWED_PRIVATE_HOSTS` is unset or has no parseable entries + * (default — full SSRF block enforced). Otherwise returns a structure with + * the lowercase hostnames and pre-parsed CIDR ranges to match against. + * + * Each entry can be: + * - A bare hostname (e.g., `gitlab.allot.internal`) — matched against the + * URL's original hostname, case-insensitive. + * - A literal IPv4/IPv6 address (e.g., `10.112.12.56`) — matched as a /32 or /128. + * - A CIDR range (e.g., `10.0.0.0/8`, `fd00::/8`) — matched against the + * resolved IP after DNS lookup. + * + * The result is cached for the process lifetime; env changes require a restart. + */ +export function getAllowedPrivateHostsFromEnv(): AllowedPrivateHosts | null { + if (cachedAllowedPrivateHosts !== undefined) return cachedAllowedPrivateHosts + if (!env.ALLOWED_PRIVATE_HOSTS) { + cachedAllowedPrivateHosts = null + return null + } + const hostnames = new Set() + const cidrs: AllowedPrivateHosts['cidrs'] = [] + for (const raw of env.ALLOWED_PRIVATE_HOSTS.split(',')) { + const entry = raw.trim() + if (!entry) continue + if (entry.includes('/')) { + try { + cidrs.push(ipaddr.parseCIDR(entry)) + continue + } catch { + // fall through and treat as hostname + } + } + if (ipaddr.isValid(entry)) { + const addr = ipaddr.process(entry) + cidrs.push([addr, addr.kind() === 'ipv4' ? 32 : 128]) + continue + } + hostnames.add(entry.toLowerCase()) + } + if (hostnames.size === 0 && cidrs.length === 0) { + cachedAllowedPrivateHosts = null + return null + } + cachedAllowedPrivateHosts = { hostnames, cidrs } + return cachedAllowedPrivateHosts +} + +/** + * Returns true if either the original hostname or the resolved IP appears in + * the operator-curated `ALLOWED_PRIVATE_HOSTS` allowlist. + * + * Lets self-hosted deployments call internal services (e.g., GitLab on a 10.x + * address) without disabling SSRF protection entirely. + * + * The caller should still run the standard private-IP check first; this + * function is meant as an override gate after a block decision, not a + * replacement for SSRF validation. When the env var is unset, returns false + * and the default block stands. + */ +export function isAllowlistedPrivateHost(opts: { hostname?: string; ip?: string }): boolean { + const allow = getAllowedPrivateHostsFromEnv() + if (!allow) return false + if (opts.hostname && allow.hostnames.has(opts.hostname.toLowerCase())) return true + if (opts.ip && ipaddr.isValid(opts.ip)) { + try { + const addr = ipaddr.process(opts.ip) + for (const range of allow.cidrs) { + if (addr.kind() !== range[0].kind()) continue + if (addr.match(range)) return true + } + } catch { + // ignore unparseable IPs — caller already handled validation + } + } + return false +} + +/** + * Test-only hook to reset the cached `ALLOWED_PRIVATE_HOSTS` parse result so + * each test can swap the underlying env value without process restart. + */ +export function __resetAllowedPrivateHostsCacheForTest(): void { + cachedAllowedPrivateHosts = undefined +} + /** * Get cost multiplier based on environment */ diff --git a/apps/sim/lib/core/security/input-validation.server.ts b/apps/sim/lib/core/security/input-validation.server.ts index e16bda7c6ea..258c2bb42c6 100644 --- a/apps/sim/lib/core/security/input-validation.server.ts +++ b/apps/sim/lib/core/security/input-validation.server.ts @@ -5,7 +5,7 @@ import type { LookupFunction } from 'net' import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' import * as ipaddr from 'ipaddr.js' -import { isHosted } from '@/lib/core/config/feature-flags' +import { isAllowlistedPrivateHost, isHosted } from '@/lib/core/config/feature-flags' import { type ValidationResult, validateExternalUrl } from '@/lib/core/security/input-validation' const logger = createLogger('InputValidation') @@ -111,7 +111,11 @@ export async function validateUrlWithDNS( return ip === '127.0.0.1' || ip === '::1' })() - if (isPrivateOrReservedIP(address) && !(isLocalhost && resolvedIsLoopback && !isHosted)) { + if ( + isPrivateOrReservedIP(address) && + !(isLocalhost && resolvedIsLoopback && !isHosted) && + !isAllowlistedPrivateHost({ hostname: cleanHostname, ip: address }) + ) { logger.warn('URL resolves to blocked IP address', { paramName, hostname, @@ -168,14 +172,21 @@ export async function validateDatabaseHost( return { isValid: false, error: `${paramName} cannot be localhost` } } - if (ipaddr.isValid(lowerHost) && isPrivateOrReservedIP(lowerHost)) { + if ( + ipaddr.isValid(lowerHost) && + isPrivateOrReservedIP(lowerHost) && + !isAllowlistedPrivateHost({ ip: lowerHost }) + ) { return { isValid: false, error: `${paramName} cannot be a private IP address` } } try { const { address } = await dns.lookup(host, { verbatim: true }) - if (isPrivateOrReservedIP(address)) { + if ( + isPrivateOrReservedIP(address) && + !isAllowlistedPrivateHost({ hostname: lowerHost, ip: address }) + ) { logger.warn('Database host resolves to blocked IP address', { paramName, hostname: host, diff --git a/apps/sim/lib/core/security/input-validation.ts b/apps/sim/lib/core/security/input-validation.ts index 98ac9e1c982..6fc83450e2d 100644 --- a/apps/sim/lib/core/security/input-validation.ts +++ b/apps/sim/lib/core/security/input-validation.ts @@ -1,6 +1,6 @@ import { createLogger } from '@sim/logger' import * as ipaddr from 'ipaddr.js' -import { isHosted } from '@/lib/core/config/feature-flags' +import { isAllowlistedPrivateHost, isHosted } from '@/lib/core/config/feature-flags' const logger = createLogger('InputValidation') @@ -401,7 +401,7 @@ export function validateHostname( } if (ipaddr.isValid(lowerHostname)) { - if (isPrivateOrReservedIP(lowerHostname)) { + if (isPrivateOrReservedIP(lowerHostname) && !isAllowlistedPrivateHost({ ip: lowerHostname })) { logger.warn('Hostname matches blocked IP range', { paramName, hostname: hostname.substring(0, 100), @@ -411,6 +411,8 @@ export function validateHostname( error: `${paramName} cannot be a private IP address or localhost`, } } + } else if (isAllowlistedPrivateHost({ hostname: lowerHostname })) { + return { isValid: true, sanitized: lowerHostname } } const hostnamePattern = @@ -733,7 +735,7 @@ export function validateExternalUrl( } if (!isLocalhost && ipaddr.isValid(cleanHostname)) { - if (isPrivateOrReservedIP(cleanHostname)) { + if (isPrivateOrReservedIP(cleanHostname) && !isAllowlistedPrivateHost({ ip: cleanHostname })) { return { isValid: false, error: `${paramName} cannot point to private IP addresses`, diff --git a/apps/sim/lib/mcp/domain-check.test.ts b/apps/sim/lib/mcp/domain-check.test.ts index ff559caa8cf..6e2b9ea2175 100644 --- a/apps/sim/lib/mcp/domain-check.test.ts +++ b/apps/sim/lib/mcp/domain-check.test.ts @@ -4,14 +4,17 @@ import { inputValidationMock, inputValidationMockFns } from '@sim/testing' import { beforeEach, describe, expect, it, vi } from 'vitest' -const { mockGetAllowedMcpDomainsFromEnv, mockDnsLookup, hostedFlag } = vi.hoisted(() => ({ - mockGetAllowedMcpDomainsFromEnv: vi.fn<() => string[] | null>(), - mockDnsLookup: vi.fn(), - hostedFlag: { value: false }, -})) +const { mockGetAllowedMcpDomainsFromEnv, mockIsAllowlistedPrivateHost, mockDnsLookup, hostedFlag } = + vi.hoisted(() => ({ + mockGetAllowedMcpDomainsFromEnv: vi.fn<() => string[] | null>(), + mockIsAllowlistedPrivateHost: vi.fn<() => boolean>().mockReturnValue(false), + mockDnsLookup: vi.fn(), + hostedFlag: { value: false }, + })) vi.mock('@/lib/core/config/feature-flags', () => ({ getAllowedMcpDomainsFromEnv: mockGetAllowedMcpDomainsFromEnv, + isAllowlistedPrivateHost: mockIsAllowlistedPrivateHost, get isHosted() { return hostedFlag.value }, @@ -502,4 +505,56 @@ describe('validateMcpServerSsrf', () => { ).resolves.toBeNull() expect(mockDnsLookup).not.toHaveBeenCalled() }) + + describe('ALLOWED_PRIVATE_HOSTS allowlist', () => { + it('allows private IP literal when in allowlist', async () => { + mockIsAllowlistedPrivateHost.mockImplementation( + ({ ip }: { ip?: string }) => ip === '10.112.12.56' + ) + await expect(validateMcpServerSsrf('http://10.112.12.56:8080/mcp')).resolves.toBeNull() + expect(mockIsAllowlistedPrivateHost).toHaveBeenCalledWith({ ip: '10.112.12.56' }) + }) + + it('still blocks private IP literal when allowlist returns false', async () => { + mockIsAllowlistedPrivateHost.mockReturnValue(false) + await expect(validateMcpServerSsrf('http://10.0.0.5:8080/mcp')).rejects.toThrow(McpSsrfError) + }) + + it('allows hostname resolving to private IP when hostname is allowlisted', async () => { + mockIsAllowlistedPrivateHost.mockImplementation( + ({ hostname }: { hostname?: string }) => hostname === 'gitlab.allot.internal' + ) + mockDnsLookup.mockResolvedValue({ address: '10.112.12.56' }) + await expect(validateMcpServerSsrf('https://gitlab.allot.internal/mcp')).resolves.toBe( + '10.112.12.56' + ) + }) + + it('allows hostname resolving to private IP when resolved IP is allowlisted', async () => { + mockIsAllowlistedPrivateHost.mockImplementation( + ({ ip }: { ip?: string }) => ip === '10.112.12.56' + ) + mockDnsLookup.mockResolvedValue({ address: '10.112.12.56' }) + await expect(validateMcpServerSsrf('https://gitlab.allot.internal/mcp')).resolves.toBe( + '10.112.12.56' + ) + }) + + it('still blocks resolved private IP when neither hostname nor IP is allowlisted', async () => { + mockIsAllowlistedPrivateHost.mockReturnValue(false) + mockDnsLookup.mockResolvedValue({ address: '10.0.0.5' }) + await expect(validateMcpServerSsrf('https://other.internal/mcp')).rejects.toThrow( + McpSsrfError + ) + }) + + it('does not override the hosted-mode loopback block', async () => { + hostedFlag.value = true + mockIsAllowlistedPrivateHost.mockReturnValue(true) + mockDnsLookup.mockResolvedValue({ address: '127.0.0.1' }) + await expect(validateMcpServerSsrf('https://loopback.example/mcp')).rejects.toThrow( + McpSsrfError + ) + }) + }) }) diff --git a/apps/sim/lib/mcp/domain-check.ts b/apps/sim/lib/mcp/domain-check.ts index 9e57b23c7f4..ecb7440f3aa 100644 --- a/apps/sim/lib/mcp/domain-check.ts +++ b/apps/sim/lib/mcp/domain-check.ts @@ -2,7 +2,11 @@ import dns from 'dns/promises' import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' import * as ipaddr from 'ipaddr.js' -import { getAllowedMcpDomainsFromEnv, isHosted } from '@/lib/core/config/feature-flags' +import { + getAllowedMcpDomainsFromEnv, + isAllowlistedPrivateHost, + isHosted, +} from '@/lib/core/config/feature-flags' import { isPrivateOrReservedIP } from '@/lib/core/security/input-validation.server' import { createEnvVarPattern } from '@/executor/utils/reference-validation' @@ -171,7 +175,7 @@ export async function validateMcpServerSsrf(url: string | undefined): Promise