diff --git a/lib/ingest/safe_fetch.js b/lib/ingest/safe_fetch.js index e3b4d6b..28ab0b4 100644 --- a/lib/ingest/safe_fetch.js +++ b/lib/ingest/safe_fetch.js @@ -62,15 +62,16 @@ async function resolveAndCheck(host) { throw new SafeFetchError(`${host} resolves to blocked address ${r.address}`, 'blocked_addr'); } } - return records[0]; + return records; // pass all validated records to the dispatcher } -function pinnedDispatcher(address, family) { +function pinnedDispatcher(records) { return new Agent({ connect: { - // undici will call our lookup instead of OS DNS, so the chosen IP - // is the one we already validated. No TOCTOU re-resolution. - lookup: (_hostname, _options, cb) => cb(null, address, family) + // undici will call our lookup instead of OS DNS, so it only sees + // IPs we've already validated. The array form `(err, results)` is + // what undici v6+ expects (results[i] = {address, family}). + lookup: (_hostname, _options, cb) => cb(null, records) } }); } @@ -82,17 +83,14 @@ export async function safeFetch(url, options = {}, { maxHops = 5 } = {}) { if (u.protocol !== 'http:' && u.protocol !== 'https:') { throw new SafeFetchError(`unsupported scheme ${u.protocol}`, 'scheme'); } - let address, family; + let records; if (net.isIP(u.hostname)) { if (isBlockedAddr(u.hostname)) throw new SafeFetchError(`blocked literal IP ${u.hostname}`, 'blocked_addr'); - address = u.hostname; - family = net.isIPv4(u.hostname) ? 4 : 6; + records = [{ address: u.hostname, family: net.isIPv4(u.hostname) ? 4 : 6 }]; } else { - const rec = await resolveAndCheck(u.hostname); - address = rec.address; - family = rec.family; + records = await resolveAndCheck(u.hostname); } - const dispatcher = pinnedDispatcher(address, family); + const dispatcher = pinnedDispatcher(records); const res = await fetch(current, { ...options, redirect: 'manual', dispatcher }); if ([301,302,303,307,308].includes(res.status)) { const loc = res.headers.get('location');