Skip to content

HTTP-client adapter guard-coverage drift — the axios 1.20 wave (Sept 30 GHSA cluster)

Signal: GitHub Security Advisories published a ~13-advisory Axios wave on 2026-09-30 (fixed in 1.20.0) plus an Astro/Netlify allowlist bypass in the same window. One rule ties them: security controls configured at the library's top level are enforced per-adapter, and the non-default adapters are where the guards are missing. An application that picked a guard flag (maxRedirects: 0, lookup, proxy, NO_PROXY) did not buy the control — it bought the control on the adapter that happens to read the flag.

Advisory table

GHSA Sev. Boundary
GHSA-3pq3-5fj3-cg6v high HTTP/2 adapter drops lookup, agent, and proxy state. The HTTP/1 path wraps config.lookup and applies setProxy(); the httpVersion: 2 path calls http2.connect(authority, http2Options) with only http2Options — the DNS-policy resolver and the tunneling agent never reach the transport. Advisory's own verification: lookupCalls: 0 on an h2 request; an explicit rejecting proxy observed no traffic while the origin received the request. Where lookup/proxy is the SSRF guard, h2 requests bypass it entirely (>=1.13.0, <1.20.0)
GHSA-r4gj-5m52-g5wh high Fetch adapter never reads maxRedirects. maxRedirects: 0 is the standard redirect-based SSRF guard; the fetch adapter omits redirect from its options, so the Fetch API default follow applies. Same URL, same config: http adapter throws, fetch adapter returns the internal response (>=1.17.0, <1.20.0)
GHSA-44g4-m2mj-wpvx medium CIDR-form NO_PROXY entries are ignored (127.0.0.0/8, 169.254.169.254/32 match nothing), so traffic operators believed proxy-excluded — loopback, RFC1918, metadata — is routed through the proxy. Second spelling of the "operator trust-header/exclusion drift" axis (cf. Fabio, Sept 22) (>=1.15.0, <1.20.0)
GHSA-m8m8-qj5v-23w3 high Socket-level hijack gadget via inherited createConnection. The adapter builds a null-prototype options object but never sets an own createConnection; Node's option normalization can pick up Object.prototype.createConnection from a prior pollution primitive and open the socket to an attacker endpoint while the request URL and config still name the legitimate origin. Destination validation that checks the URL before calling axios is defeated — the URL is honest, the socket is not (>=1.15.2, <1.20.0)
GHSA-j8rh-479h-cp32 / GHSA-4hqw-qxg8-jxx2 medium Header injection via inherited headers after a minimal interceptor, and via fetch-adapter FormData getHeaders duck-typing — continuations of the May 5 read-side gadget family on the new adapters
GHSA-x97p-jq2g-jp4f / GHSA-vh66-26gq-q6x8 / GHSA-9fr6-4gfg-395g med/high Additional prototype-pollution gadgets: toFormData options, fetch-adapter outbound-request alteration, default-instance Object.prototype.method override
GHSA-mghh-pgcx-3jjj / GHSA-c29m-xwm3-cm6r high ReDoS pair reachable from untrusted input: O(N²) in shouldBypassProxy host normalization via a redirect Location header, and event-loop freeze in the fromDataURI data-URL parser. Proxy-bypass evaluation is a security-sensitive parse of attacker-controlled strings — it also carries a crash surface
GHSA-542g-h47m-68v8 high Unhandled error event on HTTP/2 ClientHttp2Session init → process crash (DoS on the same h2 path as 3pq3 — the adapter is under-tested across the board)
GHSA-4233-jc72-56c5 medium Astro @astrojs/netlify generated unanchored allowlist regexes. Netlify Image CDN evaluates image.domains/remotePatterns entries with RegExp.test() (substring semantics), so http://127.0.0.1:6379/?url=https://images.example.com/image.png passes the images.example.com allowlist and the CDN fetches Redis. Public /.netlify/images endpoint, unauthenticated (5.2.0–8.2.3, fixed 8.2.4). Third spelling of the Oct 1 anchor rule: regex ^/allowed prefix, startsWith(root), and now unanchored .test() allowlists — the approved origin anywhere in the URL satisfies it

Why this is durable

  1. Guard flags are adapter-scoped. Enumerate every transport an HTTP client ships (http, https, fetch, http2, xhr, WebSocket-adjacent) and re-run the same guard test battery per transport: maxRedirects: 0 against an owned 302, rejecting lookup against a denied host, explicit rejecting proxy, NO_PROXY exact-host vs CIDR vs loopback alias. A guard that passes on the default adapter and is silent on the others is the finding. Node 18+-era runtimes may auto-select the fetch adapter, so "we configured maxRedirects: 0" can be true config with no enforcement.
  2. Destination validation races the transport. URL-string checks before the call are bypassed by anything that changes socket routing (createConnection gadget, h2 authority reconstruction, proxy/agent drop). The equivalence class to test: config says A, socket reaches B.
  3. Security-policy parsers carry their own crash/injection surface. NO_PROXY/host-normalization code parses untrusted redirect targets — ReDoS via Location means a redirecting server can freeze the event loop mid-policy-evaluation.
  4. Generated allowlist regexes are codegen output — audit the artifact, not the config. In any platform that compiles domain lists into regexes (CDN image allowlists, WAF rules, OAuth redirect matchers), fetch or reconstruct the generated expression and check anchors (^...$), backtracking semantics, and where the matcher runs. .test() without anchors is a substring matcher wearing an allowlist costume.

Hunt ideas (authorized scope)

  • Grep Next/Node services for axios usage where the SSRF posture depends on lookup, proxy, or maxRedirects — for each, determine the effective adapter (adapter: 'fetch' explicit, runtime auto-selection, httpVersion: 2) and replay the guard battery on that adapter against an owned redirector and an owned loopback listener. Evidence: adapter-vs-guard decision table.
  • Where outbound policy uses NO_PROXY, enumerate destinations the operator meant to exclude (metadata IPs, RFC1918, loopback) and test CIDR entries with an owned listener on a host inside the range; the proxy-observability differential (does the proxy see the request?) is the verdict.
  • In a lab process that already has a pollution primitive, add createConnection, headers, toFormData keys to the canary set and assert requests stay on-path. Where they don't, report the gadget chain (pollution source → axios sink → socket destination), fake credentials only.
  • For any Netlify Image CDN (or analogous generated-allowlist image proxy): request /.netlify/images?url=<owned-allowlisted-origin embedded in query/path of a target URL> and confirm the fetch lands where the parser put it, using your own listener as the target. Response-as-image transformation limits exfil — prove reachability, don't chase data.
  • Redirect-handling code paths in any client/proxy: fuzz Location with pathological host spellings for latency/crash before assuming policy parsing is inert.

Validation boundaries

Owned redirectors, owned loopback listeners, fake credentials, and disposable deployments only. Never route through third-party proxy infrastructure to demonstrate NO_PROXY drift, never target cloud metadata endpoints to prove reachability, never leave polluted prototypes in shared processes. Upgrade target: axios 1.20.0; @astrojs/netlify 8.2.4 (or imageCDN: false).

Tracked in this wave, not published

league/commonmark DisallowedRawHtml bypass when the disallowed tag name ends the raw-HTML literal (97jj, parser-state sanitizer differential — fold on the next sanitizer cluster); jackson-databind quadratic forward-reference + unknown-type-ID retention pair (cxp5/wv8q, DoS class); Serialize-javascript </script> in serialized function bodies (gfhx) and DOMPurify IN_PLACE detached-handler arming (p98j) — revisit with concrete sink pairing; Astro Host-port Node-adapter crash (qh8j, DoS); Tornado query-arg-count and CurlAsyncHTTPClient decompression DoS pair (3hv7/chx6).

October 1 12:31Z follow-up: same rule, JAXP factories (Camel Quarkus XSLT XXE)

CVE-2026-88789 / GHSA-w8wc-5pf9-6ph5 (high, camel-quarkus-support-xalan 3.2.0–<3.33.3, 3.34.0–<3.40.0) extends this page's invariant from HTTP adapters to XML stacks: Apache Camel applies ACCESS_EXTERNAL_DTD/ACCESS_EXTERNAL_STYLESHEET hardening to the TransformerFactory it creates, but the XSLT support extension supplies its own Xalan-J 2.7.x-backed factory and registers it as the JAXP default — a factory that predates JAXP 1.5 and ignores those flags entirely. Result: any untrusted XML body that reaches the xslt component as a javax.xml.transform.Source gets local-file read / internal-network requests via external-entity declarations, while the framework believes its hardening is on. Audit rule: in any JVM stack with pluggable factory providers (JAXP TransformerFactory/SAXParserFactory, JCE providers, JDBC drivers), check which factory instance the data path actually uses — a legacy factory registered as the global default silently defeats per-framework security configuration. Proof stays lab-only: owned HTTP callback entity + local marker file in a disposable Camel/Quarkus service, never production integration endpoints.

Second same-shape record from the Oct 1 12:31Z wave: Budibase AI table generation (CVE-2026-103757 / GHSA-2524-3r74-44wr, high, ≤3.41.0) — the platform ships a blacklisting fetch helper (fetchWithBlacklist), but uploadUrl in packages/server/src/utilities/fileUtils.ts calls raw node-fetch; an authenticated builder user's prompt to POST /api/ai/tables places an internal URL in an attachment column and the server fetches it, returning a presigned object-storage URL that carries the response (cloud metadata credentials named in the advisory). Guard-coverage sweep, application tier: grep the codebase for the direct client import (node-fetch, got, requests, fetch() alongside the sanctioned helper — every call site using the raw client is an unguarded fetch leg, whatever the platform's SSRF posture claims. On closed-source targets the black-box equivalent: enumerate features that "attach a URL" and diff which ones obey your denylist canary (owned rebinding listener) versus which don't. Prove with lab-only canaries; never read real metadata.