fix(door): defeat DNS-rebind by connecting to a validated resolved IP (D12)

Deferred deeper-audit P1. validateDoorTarget only string-checked the
host; a bare fetch did its own DNS resolution, so a hostname could
resolve to 127.0.0.1/169.254.169.254 between check and connect. The
executor now resolves once (dns.lookup all), rejects if ANY resolved
address is blocked (isBlockedDoorIp: loopback/unspecified/link-local/
metadata incl IPv4-mapped IPv6; RFC1918/LAN/ULA allowed since door
controllers live there), then connects to the validated IP literal so
fetch performs no second resolution (TOCTOU closed). Original Host kept
best-effort; redirect:'manual' retained. Dependency-free (no undici
seam). 33 door-logic tests; suite 532.
This commit is contained in:
Moheeb Zara
2026-05-19 01:33:01 -07:00
parent 705f03a299
commit 42fd3859a4
3 changed files with 88 additions and 4 deletions
+24
View File
@@ -3,6 +3,7 @@ import {
last4,
maskCardUid,
isAlwaysBlockedHost,
isBlockedDoorIp,
normalizeHost,
validateDoorTarget,
encodeHeatSyncGrant,
@@ -32,6 +33,29 @@ describe('maskCardUid', () => {
})
})
describe('isBlockedDoorIp (resolved-IP DNS-rebind guard)', () => {
it('blocks loopback / unspecified / link-local / metadata (v4)', () => {
for (const ip of ['127.0.0.1', '127.1.2.3', '0.0.0.0', '0.1.2.3', '169.254.1.2', '169.254.169.254']) {
expect(isBlockedDoorIp(ip)).toBe(true)
}
})
it('blocks v6 loopback/unspecified/link-local/AWS-IMDS + IPv4-mapped', () => {
for (const ip of ['::1', '::', 'fe80::1', 'fd00:ec2::254', '::ffff:127.0.0.1', '::ffff:169.254.169.254', '[::1]']) {
expect(isBlockedDoorIp(ip)).toBe(true)
}
})
it('ALLOWS RFC1918 / LAN / ULA / public (door controllers live there)', () => {
for (const ip of ['10.0.0.5', '172.16.4.4', '192.168.1.50', '203.0.113.7', 'fd12:3456::1', '2001:db8::1', '::ffff:192.168.1.9']) {
expect(isBlockedDoorIp(ip)).toBe(false)
}
})
it('fails closed on unrecognizable / malformed input', () => {
for (const ip of ['', 'not-an-ip', '999.1.1.1', '1.2.3', 'door.local']) {
expect(isBlockedDoorIp(ip)).toBe(true)
}
})
})
describe('isAlwaysBlockedHost', () => {
it('blocks cloud metadata and link-local', () => {
expect(isAlwaysBlockedHost('169.254.169.254')).toBe(true)