Summary
axios 1.18.1 in this project's dependency tree carried CVE-2026-101898: on the HTTP/2 request path, the proxy and DNS settings supplied in axios request configuration were not applied. The lockfile now resolves axios at 1.20.0, which carries the upstream fix.
The important detail is what kind of bug this is. Nothing here crashes, corrupts memory, or injects a payload. Instead, a security control that developers deliberately configured — "all outbound traffic must go through this proxy", "resolve hostnames with this lookup function so internal IPs are rejected" — was quietly not honoured for a subset of requests. Controls that fail open are worse than controls that fail loudly, because the code looks correct, the tests pass, and the audit log is simply missing entries.
Introduction
Server-side HTTP clients are where SSRF defences live. A typical hardened Node.js service does something like this:
const http2 = require('http2');
await axios.get(userSuppliedUrl, {
proxy: { host: 'egress.internal', port: 3128 },
lookup: pinnedResolver, // rejects 169.254.0.0/16, 10/8, ::1, etc.
transport: http2, // or an agent that negotiates h2 via ALPN
});
Two independent controls are in play. proxy forces every connection through an auditable, filtering egress hop. lookup replaces Node's DNS resolution so that a hostname resolving to a link-local or RFC 1918 address is refused before a socket is opened — the standard answer to DNS rebinding, where a name passes an allowlist check and then resolves to 169.254.169.254 a moment later.
CVE-2026-101898 is the discovery that, in affected axios 1.x releases, the HTTP/2 request path did not consult those settings. The proxy object, the HTTP_PROXY / HTTPS_PROXY / NO_PROXY environment variables resolved through proxy-from-env, and the custom lookup function were all ignored once a request went out over h2. The request still succeeded. It just went directly to the destination, resolved by the system resolver, with no proxy record of it ever happening.
For developers, the lesson generalises beyond axios: whenever a library grows a second transport, every security-relevant option has to be re-plumbed into it, and any option that is read on one path but not the other becomes a bypass.
Affected Versions
| Affected | unknown (this dependency tree resolved axios at 1.18.1) |
| Fixed in | 1.20.0 |
| Ecosystem | npm |
| CVE / GHSA | CVE-2026-101898 / GHSA not assigned |
| CWE | unknown |
Consult the upstream advisory for the authoritative affected range: CVE-2026-101898.
The Vulnerability Explained
This fix arrives as a lockfile change, so there is no application source to quote. The change is the resolved version of the axios dependency entry:
"node_modules/axios": {
- "version": "1.18.1",
+ "version": "1.20.0",
"dependencies": {
"follow-redirects": "^1.16.0",
- "form-data": "^4.0.5",
+ "form-data": "^4.0.6",
"https-proxy-agent": "^5.0.1",
"proxy-from-env": "^2.1.0"
}
Those dependency names tell the story. proxy-from-env is how axios turns HTTP_PROXY / NO_PROXY into a proxy decision. https-proxy-agent is how it tunnels an HTTPS request through that proxy. Both are wired into the HTTP/1.1 adapter. The HTTP/2 path in 1.18.1 did not route through the same decision, so neither of those packages participated in the connection — and the user-supplied lookup function never reached the socket options either.
The problematic pattern
The defect is not a line of dangerous code; it is a missing read of configuration on a second code path:
- HTTP/1.1 request → proxy resolution runs →
lookupis passed to the connection → control enforced. - HTTP/2 request → connection established directly →
proxyandlookupnever consulted → control skipped.
Because both paths return a normal axios response, nothing in application code can tell the difference. response.status is 200 either way.
Attack scenario
Consider a webhook-validation endpoint that fetches a customer-supplied URL, with lookup pinned to reject private ranges and proxy pointing at the corporate egress gateway:
- An attacker submits
https://attacker.example/callback, a host they control that advertises HTTP/2 via ALPN, or the service is configured to use the HTTP/2 transport for outbound calls. - axios 1.18.1 takes the HTTP/2 path. The pinned
lookupresolver is not installed on the connection, so resolution falls back to the system resolver. - The attacker's authoritative DNS answers with a short-TTL record pointing at
169.254.169.254(cloud metadata) or an internal service address. The DNS-pinning check that was supposed to block this never ran. - The proxy is also skipped, so the gateway that would have blocked the destination — and, just as importantly, logged the attempt — sees nothing.
The realistic outcomes for a service running this code:
- SSRF allowlisting defeated. A
lookup-based guard is the main defence against rebinding; bypassing it reopens access to metadata endpoints, admin interfaces on loopback, and internal-only APIs. - Egress policy and DLP bypassed. In environments where the only sanctioned path off the host is the proxy, HTTP/2 requests leave unfiltered. Exfiltration over an axios call becomes invisible to proxy logs.
- Broken detection, not just broken prevention. Incident responders reconstruct outbound activity from proxy records. Requests that never touched the proxy leave no trail.
NO_PROXYinversion. Deployments that rely on proxy environment variables for all traffic get an unintended direct-connect path, with no error and no warning.
Severity is rated high here because the bypass is silent, reachable from ordinary request configuration, and undermines a control specifically deployed against SSRF.
The Fix
The change is a single-dependency upgrade in the lockfile:
Before — axios resolved at 1.18.1, where the HTTP/2 path does not apply config.proxy, proxy environment variables, or config.lookup.
After — axios resolved at 1.20.0, which carries the upstream fix so that the HTTP/2 path honours the same proxy resolution and DNS lookup settings as the HTTP/1.1 adapter.
Two things moved in the lockfile entry, and it is worth separating them:
version1.18.1 → 1.20.0. This is the security-relevant change. It is what makesproxy,NO_PROXY, andlookupeffective on HTTP/2 requests.form-dataconstraint^4.0.5→^4.0.6. This is axios's own declared dependency metadata in 1.20.0, not an independent edit. It affects multipart encoding, not the HTTP/2 transport, and it comes along automatically with the upgrade.
No application code changes are required. The public configuration surface — proxy, lookup, transport, httpAgent, httpsAgent — is unchanged. That is precisely why this class of bug is dangerous and why the fix is cheap: call sites were already written correctly, and the library simply starts respecting them.
One caveat on verification. This fix was proposed from dependency metadata. The presence of axios 1.18.1 in the dependency tree is established; whether your code actually reaches the HTTP/2 request path was not verified. If you never enable an HTTP/2 transport and never use an agent that negotiates h2 over ALPN, your practical exposure is lower — but the upgrade is still the correct action, because transport selection can change with an agent swap or a dependency's defaults.
Verifying the upgrade landed:
npm ls axios # expect 1.20.0, and check for nested duplicates
A nested axios under another package can remain on the vulnerable line even after the top-level entry is updated. If npm ls axios prints more than one version, resolve the transitive copy too.
Key Takeaways
config.proxyandconfig.lookupin axios 1.18.1 were applied on the HTTP/1.1 path but not the HTTP/2 path — if those options are your SSRF control, they were not enforced for h2 requests.- A security option that is accepted without error is not the same as one that is applied. Test enforcement, not acceptance: assert that a blocked internal address actually fails and that the proxy log contains the request.
- DNS-pinning via a custom
lookupfunction only protects connections where the resolver is actually installed on the socket. It is a client-side control with client-side failure modes. - Treat "the library gained a new transport" as a security event. Every proxy, DNS, TLS, and timeout option has to be re-plumbed into the new path, and any option read on only one path is a bypass.
- Enforce egress at the network boundary — security groups, firewall rules, a transparent proxy — so that an in-process client bug cannot turn into unmonitored outbound traffic.
How Orbis AppSec Detected This
- Source: the request URL and request configuration passed to the axios request API (
axios.get(),axios.request(), instance methods), includingproxy,lookup, andtransport, where the target host may be influenced by untrusted input such as a user-supplied webhook or callback URL. - Sink: the outbound connection established on the axios HTTP/2 request path, which in 1.18.1 opened the socket without consulting the configured proxy resolution (
proxy-from-env/https-proxy-agent) or the custom DNSlookupfunction. - Missing control: the HTTP/2 path did not read or apply the proxy and DNS settings from request configuration, so proxy enforcement and DNS-pinning allowlist checks were skipped for those requests with no error surfaced to the caller.
- CWE: unknown — no CWE identifier is recorded in the advisory data available for this finding. Behaviourally, it is a configured protection mechanism that is not applied on a specific code path.
- Fix: upgrade
axiosfrom 1.18.1 to 1.20.0, where the HTTP/2 path applies the same proxy and DNSlookupsettings as the HTTP/1.1 adapter.
Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.
Conclusion
CVE-2026-101898 is a reminder that the most expensive vulnerabilities are not always the loudest ones. No crash, no injected payload, no stack trace — just a proxy object and a lookup function that axios 1.18.1 accepted and then ignored on HTTP/2 connections, turning a deliberate SSRF defence into decoration.
The remediation is a one-line lockfile change to axios 1.20.0, with the form-data constraint moving from ^4.0.5 to ^4.0.6 as a consequence of the upgrade. After upgrading, confirm with npm ls axios that no nested copy is still on the vulnerable line, and add a test that proves your egress proxy and DNS pinning actually reject a blocked destination rather than merely being configured.