fix: prevent DNS-rebinding TOCTOU in safeProxyFetch by pinning resolved IPs#1732
Open
manjunathbhaskar wants to merge 1 commit into
Open
Conversation
manjunathbhaskar
force-pushed
the
fix/dns-rebinding-toctou-safeproxyfetch
branch
from
July 21, 2026 10:45
1d380ef to
5cccad2
Compare
…ed IPs Previously assertSafeProxyTarget resolved the target hostname and validated the IPs, but safeProxyFetch then passed the raw URL to node-fetch, which performed its own DNS lookup. In the window between the two resolutions an attacker who controls the domain's TTL could flip the record to 169.254.169.254 (cloud-metadata), causing fetch() to connect to the instance- metadata service even though the block-list check passed. Fix: assertSafeProxyTarget now returns the validated IP addresses. On each hop of safeProxyFetch we call createPinnedAgent() to build an http/https.Agent whose lookup hook unconditionally returns the pre-validated IP. node-fetch uses that hook instead of the OS resolver, closing the TOCTOU window entirely. Refactoring: isBlockedProxyAddress, assertSafeProxyTarget, and the new createPinnedAgent are extracted to server/src/proxy-security.ts so they can be unit-tested in isolation. A vitest suite is added to the server package (the first unit tests for this package) with 26 cases covering the block-list, DNS validation, IP-pinning, and the TOCTOU guarantee itself.
manjunathbhaskar
force-pushed
the
fix/dns-rebinding-toctou-safeproxyfetch
branch
from
July 21, 2026 10:46
5cccad2 to
9140789
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
safeProxyFetchcontains a TOCTOU (time-of-check / time-of-use) race that lets a DNS-rebinding attack reach cloud instance-metadata services (169.254.169.254).Attack flow (before this fix)
evil.example.comwith a very short TTL.evil.example.com./fetchproxy callsassertSafeProxyTarget("evil.example.com")→ DNS resolves to1.2.3.4→ passes block-list check ✅169.254.169.254before the TTL expires.node-fetchresolvesevil.example.comagain (its own lookup) → gets169.254.169.254→ connects to the AWS/GCP/Azure metadata endpoint.The vulnerability exists because
assertSafeProxyTargetandnode-fetcheach perform an independent DNS resolution. The gap between them is the attack window.Solution
assertSafeProxyTargetnow returns the list of validated IP addresses instead of returningvoid. On each hop ofsafeProxyFetch, the first validated IP is passed tocreatePinnedAgent(), which builds anhttp.Agent/https.Agentwhoselookuphook unconditionally returns that IP.node-fetchcalls the hook instead of the OS resolver, so there is never a second DNS lookup — the TOCTOU window is eliminated.Changes
New file:
server/src/proxy-security.tsSecurity helpers extracted from
index.tsso they can be unit-tested in isolation:isBlockedProxyAddress(ip)— unchanged logic, now exportedassertSafeProxyTarget(url)— now returnsstring[](validated IPs)createPinnedAgent(protocol, ip)— new: builds an http/https agent pinned to a specific IPModified:
server/src/index.tsproxy-security.tssafeProxyFetchcallscreatePinnedAgenton every hop using the IPs returned byassertSafeProxyTargetNew file:
server/src/__tests__/proxy-security.test.tsFirst unit-test suite for the server package (previously had zero tests). 26 test cases across 3 suites:
isBlockedProxyAddressassertSafeProxyTargetcreatePinnedAgentNew files:
server/vitest.config.ts,server/package.json(test scripts + vitest devDep)Testing
All 26 new unit tests pass. Existing integration tests in
client/src/__tests__/proxyFetchEndpoint.test.tscontinue to pass unchanged.References