diff --git a/apps/sim/app/api/tools/onepassword/utils.test.ts b/apps/sim/app/api/tools/onepassword/utils.test.ts index 4c07ede1323..8d9e9a76672 100644 --- a/apps/sim/app/api/tools/onepassword/utils.test.ts +++ b/apps/sim/app/api/tools/onepassword/utils.test.ts @@ -54,18 +54,35 @@ describe('validateConnectServerUrl', () => { }) it('blocks a hostname that resolves to a private IP', async () => { - mockDnsLookup.mockResolvedValue({ address: '10.1.2.3', family: 4 }) + mockDnsLookup.mockResolvedValue([{ address: '10.1.2.3', family: 4 }]) await expect(validateConnectServerUrl('https://connect.internal')).rejects.toThrow( 'cannot point to a private or reserved IP address' ) }) it('allows a hostname that resolves to a public IP', async () => { - mockDnsLookup.mockResolvedValue({ address: '93.184.216.34', family: 4 }) + mockDnsLookup.mockResolvedValue([{ address: '93.184.216.34', family: 4 }]) await expect(validateConnectServerUrl('https://connect.example.com')).resolves.toBe( '93.184.216.34' ) }) + + it('prefers the IPv4 address for a dual-stack host (avoids unreachable IPv6 pin)', async () => { + mockDnsLookup.mockResolvedValue([ + { address: '2606:4700::6810:85e5', family: 6 }, + { address: '93.184.216.34', family: 4 }, + ]) + await expect(validateConnectServerUrl('https://connect.example.com')).resolves.toBe( + '93.184.216.34' + ) + }) + + it('pins the sole IPv6 address for an IPv6-only host', async () => { + mockDnsLookup.mockResolvedValue([{ address: '2606:4700::6810:85e5', family: 6 }]) + await expect(validateConnectServerUrl('https://connect.example.com')).resolves.toBe( + '2606:4700::6810:85e5' + ) + }) }) describe('self-hosted deployment', () => { @@ -94,7 +111,7 @@ describe('validateConnectServerUrl', () => { }) it('allows a hostname that resolves to a private IP', async () => { - mockDnsLookup.mockResolvedValue({ address: '10.1.2.3', family: 4 }) + mockDnsLookup.mockResolvedValue([{ address: '10.1.2.3', family: 4 }]) await expect(validateConnectServerUrl('https://connect.internal')).resolves.toBe('10.1.2.3') }) }) diff --git a/apps/sim/app/api/tools/onepassword/utils.ts b/apps/sim/app/api/tools/onepassword/utils.ts index b78cf0d511c..0ecdd9abe50 100644 --- a/apps/sim/app/api/tools/onepassword/utils.ts +++ b/apps/sim/app/api/tools/onepassword/utils.ts @@ -317,7 +317,10 @@ export async function validateConnectServerUrl(serverUrl: string): Promise entry.family === 4) ?? resolved[0]).address } catch (error) { connectLogger.warn('DNS lookup failed for 1Password Connect server URL', { hostname: clean, diff --git a/apps/sim/lib/core/security/input-validation.server.ts b/apps/sim/lib/core/security/input-validation.server.ts index cfef31a08d2..f38805fe21a 100644 --- a/apps/sim/lib/core/security/input-validation.server.ts +++ b/apps/sim/lib/core/security/input-validation.server.ts @@ -188,7 +188,10 @@ export async function validateDatabaseHost( } try { - const { address } = await dns.lookup(cleanHost, { verbatim: true }) + // Prefer IPv4: pinning strips Happy Eyeballs' fallback, and a pinned IPv6 address hangs + // on IPv4-only egress (e.g. AWS NAT gateways). + const resolved = await dns.lookup(cleanHost, { all: true, verbatim: true }) + const { address } = resolved.find((entry) => entry.family === 4) ?? resolved[0] if (isPrivateOrReservedIP(address) && !isPrivateDatabaseHostsAllowed) { logger.warn('Database host resolves to blocked IP address', {