openApiTools: optional pinned-DNS private-address check (#291) - #384
Merged
Merged
Conversation
New option privateAddresses: 'refuse' (default 'allow') with allowPrivate host patterns. Operation requests and the document fetch go through an undici Agent whose connect.lookup is the shared pinned lookup, so every connection and redirect hop resolves once, checks every address and connects to the checked one. Private IP literals (every form URL normalizes, IPv4-mapped IPv6 included) and localhost names are refused when the tools are made and on every hop. 'refuse' cannot be combined with a custom fetch and throws a ConfigurationError where undici cannot load.
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.
Summary
Closes #291 — follow-up to N13a's web-fetch / http_request hardening (#255, #277).
Adds
openApiTools({ privateAddresses: 'refuse' })(default'allow', unchanged behavior) with anallowPrivatehost-pattern list, using the same pinned DNS lookup ashttp_requestandweb_fetch(src/security/privateAddress.ts):Agentwhoseconnect.lookupresolves each host once, checks every returned address, and connects the socket to the checked address — each redirect hop included. A DNS-rebinding name (public to the check,127.0.0.1to the connection) never reaches a private address.URLnormalizes (decimal, octal, hex, IPv4-mapped IPv6) andlocalhost/*.localhostare refused when the tools are made and on every hop, as aConfigurationError.allowPrivatelists hosts (intranet.example,*.corp.example, an IP address) that may be or resolve to private addresses.'refuse'cannot be combined with a customfetch— a custom fetch resolves names itself, so the check could not pin the address it connects to; making the tools throws instead of checking only half.'refuse'are made (LOU-D19); where it cannot load, making the tools throws aConfigurationErrorrather than sending unchecked requests. The Worker bundle is unaffected (the unused export is tree-shaken — the cloudflare adapter's zero-node:-reference tests pass).Default stays
'allow': base URLs are developer-configured and often internal, so the check is opt-in per the issue's API decision.Tests
New
src/tools/openapi/openApiTools.privateAddress.test.ts(33 tests, fully offline — stubbed resolver + a local node:http server that counts TCP connections):127.0.0.1second) is resolved exactly once and never reaches the local server;allowPrivateexemptions;fetch,allowPrivatewithout'refuse', invalid patterns, unknown mode) and the no-undici runtime path.Docs
'refuse'semantics; options table gainsprivateAddresses/allowPrivate.npm run docs:llms;docs:llms:checkpasses).Verification
npx vitest run src/tools/openapi/— 63/63 passnpx vitest run src/security/privateAddress.test.ts src/deploy/adapters/cloudflare.test.ts src/providers/importGraph.test.ts— 101/101 pass (incl. Worker bundle node-builtin leak check)npm run build,npm run typecheck,eslint src/tools/openapi/— cleantypecheck:testshas 133 pre-existing errors on base (unchanged; none in the new file)