Repository navigation
Refuse non-public destinations in http.request - #2092
Merged
Merged
Conversation
http.request sent through the default HttpClient: no address check and auto-followed redirects. Its URL is routinely built from trigger text or an upstream model's output, and any team Member can author one, so a workflow could read the worker's loopback services and cloud metadata (every spelling: localhost, 2130706433, 0x7f000001, 127.1, [::1], [::ffff:127.0.0.1]) and return the response as node outputs. A 302 from an allowed host to loopback was followed, and the author's X-Api-Key, PRIVATE-TOKEN and Cookie headers went along to the new origin, since the built-in follower strips only Authorization. The node now uses a named client whose ConnectCallback resolves the host and dials only public addresses, or ones an operator committed to OutboundHttp:AllowedInternalDestinations (hosts or CIDRs). Checking at connect time covers every address spelling, every redirect hop and DNS rebinding in one place, because the resolution checked is the one dialled. IPv4 reuses the sandbox egress predicate; IPv6 is global unicast only, and v4-mapped, v4-compatible, NAT64 and 6to4 forms are judged by the IPv4 address they carry. The worker's HTTP(S)_PROXY is still honoured, as the default client did, so a worker that reaches the internet only through a proxy keeps working. A connection to the proxy is dialled as operator infrastructure; a request the proxy carries has its host resolved and checked on the worker before it is sent. The proxy resolves the name again, a rebinding window only the proxy's own egress rules can close. The handler keeps no cookie jar: the factory shares one primary handler across every run and team, so a jar sent one run's session on another's requests. An author-written Host header is dropped, because Host and the SNI derived from it pick the virtual host behind an admitted name. Redirects are followed by a handler, at most 10 hops (the built-in allows 50) and never https->http. A hop that changes scheme, host or port drops every request header, a same-host http->https move included; the author keeps them by writing the https URL. A redirect that would carry the body to another origin (307/308, or 300/301/302 of anything but a POST) is returned as the node's 3xx, since the body holds the same secrets the headers do. The allowlist is parsed while the host is built, so a malformed entry stops the host instead of failing every http.request call at run time. A refusal is an IFailure (outbound_destination_refused) and fails the node as non-retryable. http.request is the only workflow-controlled outbound HTTP: git.* nodes call the bound provider, llm.complete the credential's base URL, pack import already allowlists https hosts with redirects off, and chat.post_message makes no outbound call.
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
http.requestbuilt its URL from trigger text or upstream model output and sent it through the default HttpClient, with no address check and auto-followed redirects. Any team member's workflow could read the worker's loopback services and cloud metadata, and a cross-origin redirect forwarded the author'sX-Api-Key/PRIVATE-TOKEN/Cookieheaders. The node now uses a guarded named client (GuardedHttpClientRegistration):ConnectCallbackdials only public addresses, or addresses an operator commits toOutboundHttp:AllowedInternalDestinations(hosts or CIDRs). This covers every address spelling, every redirect hop and DNS rebinding.Hostheader is dropped.outbound_destination_refusedand fail the node as non-retryable.HTTP(S)_PROXY/NO_PROXYis still honoured, so proxy-only workers keep reaching public APIs. Each proxied hop's host is resolved and checked on the worker before it is sent. The proxy re-resolves the name; this window is documented inOutboundDestinationGuardand the appsettings_doc.OriginScopedRedirectHandler, which allows at most 10 hops and never https->http.AddGuardedHttpClient(name, IConfiguration)inStartup), so a malformed entry stops the host instead of failing every call at run time.Test plan
Hostoverride ignoredhttp.request: 63/63.WebApplicationFactory<Program>): the real host'shttp.requestclient refuses loopback (pins theStartupregistration and the committed empty allowlist), and a malformed committed entry stops the host from starting.Category=E2E&Surface=Http: 165/165.HTTP(S)_PROXYset, confirm a publichttp.requestcall goes through the proxy.