mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
fix(security): prefer IPv4 when pinning DB and 1Password connections (#5799)
Same class of bug fixed for MCP in #5798: SSRF pinning forces a single resolved IP, which strips Happy Eyeballs' IPv4 fallback. dns.lookup(verbatim) returns the IPv6 address first for dual-stack hosts, so a database host or 1Password Connect server that is dual-stack gets pinned to IPv6 — unreachable on IPv4-only egress (AWS NAT gateways) and hangs until timeout. validateDatabaseHost and validateConnectServerUrl now resolve all addresses and prefer IPv4; IPv6-only hosts still pin their sole address. SSRF validation of the pinned IP is unchanged (the selected address is the one validated and pinned).
This commit is contained in:
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -191,7 +191,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', {
|
||||
|
||||
Reference in New Issue
Block a user