2
0
mirror of https://github.com/tenrok/axios.git synced 2026-06-17 19:21:29 +03:00

fix(http): preserve basic auth on same-origin redirects (#6929) (#10929)

* fix(http): preserve basic auth on same-origin redirects (#6929)

* docs(http): address redirect auth review nits

---------

Co-authored-by: Jason Saayman <jasonsaayman@gmail.com>
This commit is contained in:
Devendra Reddy Pennabadi
2026-05-27 00:19:23 +05:30
committed by GitHub
parent 5aa9da03dc
commit 58d8a125bf
3 changed files with 115 additions and 2 deletions
+1
View File
@@ -9,6 +9,7 @@
## Bug Fixes
- **AxiosHeaders:** Silently skip empty response header names emitted by some React Native Android responses instead of throwing. (**#6959**, **#10875**)
- **HTTP Adapter - Auth on Redirect:** HTTP Basic credentials supplied via `config.auth` are now restored on same-origin redirects, fixing a regression caused by `follow-redirects` >= 1.15.8 that broke `POST` requests answered with a 303 Location. Cross-origin redirects continue to drop credentials, preserving the existing T-R2 mitigation in `THREATMODEL.md`. (**#6929**)
- **HTTP Adapter - Socket Path:** Ignore inherited `socketPath` and `allowedSocketPaths` config values when building Node.js requests, preventing prototype-pollution SSRF via Unix sockets. (**#10901**)
- **React Native FormData:** Clear the default `Content-Type` header for React Native `FormData` requests so Android can build multipart bodies with the correct boundary. (**#10898**)
- **Request Data:** Preserve enumerable symbol keys when merging plain request data before `transformRequest`. (**#6392**)
+22 -2
View File
@@ -146,8 +146,8 @@ const flushOnFinish = (stream, [throttled, flush]) => {
const http2Sessions = new Http2Sessions();
/**
* If the proxy or config beforeRedirects functions are defined, call them with the options
* object.
* If the proxy, auth, or config beforeRedirects functions are defined, call them
* with the options object.
*
* @param {Object<string, any>} options - The options object that was passed to the request.
*
@@ -157,6 +157,9 @@ function dispatchBeforeRedirect(options, responseDetails, requestDetails) {
if (options.beforeRedirects.proxy) {
options.beforeRedirects.proxy(options);
}
if (options.beforeRedirects.auth) {
options.beforeRedirects.auth(options);
}
if (options.beforeRedirects.config) {
options.beforeRedirects.config(options, responseDetails, requestDetails);
}
@@ -860,6 +863,23 @@ export default isHttpAdapterSupported &&
if (configBeforeRedirect) {
options.beforeRedirects.config = configBeforeRedirect;
}
if (auth) {
// Restore HTTP Basic credentials on same-origin redirects only.
// follow-redirects >= 1.15.8 strips Authorization on every redirect (see #6929);
// cross-origin stripping is the documented mitigation for T-R2 in THREATMODEL.md
// and is preserved by deliberately not restoring on origin change.
const requestOrigin = parsed.origin;
const authToRestore = auth;
options.beforeRedirects.auth = function beforeRedirectAuth(redirectOptions) {
try {
if (new URL(redirectOptions.href).origin === requestOrigin) {
redirectOptions.auth = authToRestore;
}
} catch (e) {
// ignore malformed URL: leaving auth stripped is fail-safe
}
};
}
transport = isHttpsRequest ? httpsFollow : httpFollow;
}
}
+92
View File
@@ -1165,6 +1165,98 @@ describe('supports http with nodejs', () => {
}
});
it('should preserve basic auth across same-origin 303 POST -> GET redirect', async () => {
const server = await startHTTPServer(
(req, res) => {
if (req.url === '/login') {
res.setHeader('Location', '/profile');
res.statusCode = 303;
res.end();
return;
}
res.end(req.headers.authorization || '');
},
{ port: SERVER_PORT }
);
try {
const auth = { username: 'foo', password: 'bar' };
const response = await axios.post(
`http://localhost:${server.address().port}/login`,
{ hello: 'world' },
{ auth, maxRedirects: 1 }
);
const base64 = Buffer.from('foo:bar', 'utf8').toString('base64');
assert.strictEqual(response.data, `Basic ${base64}`);
assert.strictEqual(response.request.path, '/profile');
} finally {
await stopHTTPServer(server);
}
});
it('should strip basic auth on cross-origin redirect', async () => {
const targetServer = await startHTTPServer(
(req, res) => {
res.end(req.headers.authorization || 'no-auth');
},
{ port: ALTERNATE_SERVER_PORT }
);
const redirectServer = await startHTTPServer(
(req, res) => {
res.setHeader('Location', `http://127.0.0.1:${targetServer.address().port}/`);
res.statusCode = 302;
res.end();
},
{ port: SERVER_PORT }
);
try {
const auth = { username: 'foo', password: 'bar' };
const response = await axios.get(`http://localhost:${redirectServer.address().port}/start`, {
auth,
maxRedirects: 1,
});
assert.strictEqual(response.data, 'no-auth');
} finally {
await stopHTTPServer(redirectServer);
await stopHTTPServer(targetServer);
}
});
it('should preserve basic auth across multi-hop same-origin redirects', async () => {
const server = await startHTTPServer(
(req, res) => {
if (req.url === '/a') {
res.setHeader('Location', '/b');
res.statusCode = 302;
res.end();
return;
}
if (req.url === '/b') {
res.setHeader('Location', '/c');
res.statusCode = 302;
res.end();
return;
}
res.end(req.headers.authorization || '');
},
{ port: SERVER_PORT }
);
try {
const auth = { username: 'foo', password: 'bar' };
const response = await axios.get(`http://localhost:${server.address().port}/a`, {
auth,
maxRedirects: 5,
});
const base64 = Buffer.from('foo:bar', 'utf8').toString('base64');
assert.strictEqual(response.data, `Basic ${base64}`);
assert.strictEqual(response.request.path, '/c');
} finally {
await stopHTTPServer(server);
}
});
it('should provides a default User-Agent header', async () => {
const server = await startHTTPServer(
(req, res) => {