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) * docs(http): address redirect auth review nits --------- Co-authored-by: Jason Saayman <jasonsaayman@gmail.com>
This commit is contained in:
committed by
GitHub
parent
5aa9da03dc
commit
58d8a125bf
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user