feat(api): proxyUrl for residential/custom proxy egress on the API block (#5867)

* feat(api): add proxyUrl for residential/custom proxy egress on the API block

The HTTP/API block egresses from the app runtime's fixed datacenter IPs via
secureFetchWithPinnedIP, so targets behind Cloudflare/WAF that block datacenter
IPs (e.g. state .gov license portals) return 403/429 even when the identical
request works from a browser. There was no way to route a request through a
residential/custom proxy.

Add an optional `proxyUrl` field (Advanced) to the API block. When set, the
request routes through the given http:// proxy so it egresses from that proxy's
IP.

Security:
- validateAndPinProxyUrl resolves the proxy host's DNS and blocks
  private/reserved/loopback IPs (same SSRF guard as target URLs), then pins the
  connection by rewriting the host to the resolved IP (creds/port preserved),
  closing the DNS-rebinding window.
- Restricted to the http: proxy scheme (https/socks rejected) so host pinning is
  safe without breaking TLS-to-proxy SNI.
- Target-IP pinning is intentionally bypassed when a proxy is active (the proxy
  resolves the target); target URL validation still runs.

Threaded block field -> http tool param -> formatRequestParams ->
executeToolRequest (validate + pin) -> secureFetchWithPinnedIP, which swaps its
pinned Node agent for HttpsProxyAgent/HttpProxyAgent (keyed off target protocol)
when proxyUrl is set.

* docs(api): document the Proxy URL advanced field and steer proxy credentials to env vars

* fix(api): reject loopback/private proxy hosts unconditionally, closing the self-hosted rebinding gap

* chore(api): tighten proxy-path inline comments

---------

Co-authored-by: Marcus Chandra <mzxchandra@gmail.com>
This commit is contained in:
Waleed
2026-07-22 16:24:31 -07:00
committed by GitHub
co-authored by Marcus Chandra
parent 21ac7b1bba
commit 8cce661a37
13 changed files with 278 additions and 6 deletions
@@ -13,6 +13,7 @@ import { vi } from 'vitest'
*/
export const inputValidationMockFns = {
mockValidateUrlWithDNS: vi.fn(),
mockValidateAndPinProxyUrl: vi.fn(),
mockValidateDatabaseHost: vi.fn(),
mockSecureFetchWithPinnedIP: vi.fn(),
mockSecureFetchWithValidation: vi.fn(),
@@ -30,6 +31,7 @@ export const inputValidationMockFns = {
*/
export const inputValidationMock = {
validateUrlWithDNS: inputValidationMockFns.mockValidateUrlWithDNS,
validateAndPinProxyUrl: inputValidationMockFns.mockValidateAndPinProxyUrl,
validateDatabaseHost: inputValidationMockFns.mockValidateDatabaseHost,
secureFetchWithPinnedIP: inputValidationMockFns.mockSecureFetchWithPinnedIP,
secureFetchWithValidation: inputValidationMockFns.mockSecureFetchWithValidation,