Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions apps/sim/app/api/tools/onepassword/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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')
})
})
Expand Down
5 changes: 4 additions & 1 deletion apps/sim/app/api/tools/onepassword/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,10 @@ export async function validateConnectServerUrl(serverUrl: string): Promise<strin

let address: string
try {
;({ address } = await dns.lookup(clean, { 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(clean, { all: true, verbatim: true })
address = (resolved.find((entry) => entry.family === 4) ?? resolved[0]).address
} catch (error) {
connectLogger.warn('DNS lookup failed for 1Password Connect server URL', {
hostname: clean,
Expand Down
5 changes: 4 additions & 1 deletion apps/sim/lib/core/security/input-validation.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', {
Expand Down
Loading