Skip to content

Commit 2a51828

Browse files
afurmjasonsaayman
andauthored
fix(http): decode URL basic auth credentials (#10825)
* fix(http): decode URL basic auth credentials * fix(http): guard URL credential decode * chore: apply small nits --------- Co-authored-by: Jay <jasonsaayman@gmail.com>
1 parent 0e8b6bb commit 2a51828

2 files changed

Lines changed: 56 additions & 2 deletions

File tree

lib/adapters/http.js

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,22 @@ const supportedProtocols = platform.protocols.map((protocol) => {
5757
return protocol + ':';
5858
});
5959

60+
// Node's WHATWG URL parser returns `username` and `password` percent-encoded.
61+
// Decode before composing the `auth` option so credentials such as
62+
// `my%40email.com:pass` are sent as `my@email.com:pass`. Falls back to the
63+
// original value for malformed input so a bad encoding never throws.
64+
const decodeURIComponentSafe = (value) => {
65+
if (!utils.isString(value)) {
66+
return value;
67+
}
68+
69+
try {
70+
return decodeURIComponent(value);
71+
} catch (error) {
72+
return value;
73+
}
74+
};
75+
6076
const flushOnFinish = (stream, [throttled, flush]) => {
6177
stream.on('end', flush).on('error', flush);
6278

@@ -650,8 +666,8 @@ export default isHttpAdapterSupported &&
650666
}
651667

652668
if (!auth && parsed.username) {
653-
const urlUsername = parsed.username;
654-
const urlPassword = parsed.password;
669+
const urlUsername = decodeURIComponentSafe(parsed.username);
670+
const urlPassword = decodeURIComponentSafe(parsed.password);
655671
auth = urlUsername + ':' + urlPassword;
656672
}
657673

tests/unit/adapters/http.test.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -890,6 +890,44 @@ describe('supports http with nodejs', () => {
890890
}
891891
});
892892

893+
it('should decode basic auth credentials from the request URL', async () => {
894+
const server = await startHTTPServer(
895+
(req, res) => {
896+
res.end(req.headers.authorization);
897+
},
898+
{ port: SERVER_PORT }
899+
);
900+
901+
try {
902+
const response = await axios.get(
903+
`http://my%40email.com:pa%24ss@localhost:${server.address().port}/`
904+
);
905+
const base64 = Buffer.from('my@email.com:pa$ss', 'utf8').toString('base64');
906+
assert.strictEqual(response.data, `Basic ${base64}`);
907+
} finally {
908+
await stopHTTPServer(server);
909+
}
910+
});
911+
912+
it('keeps malformed URL credentials percent-encoding and does not throw', async () => {
913+
const server = await startHTTPServer(
914+
(req, res) => {
915+
res.end(req.headers.authorization);
916+
},
917+
{ port: SERVER_PORT }
918+
);
919+
920+
try {
921+
const response = await axios.get(
922+
`http://user%:foo%zz@localhost:${server.address().port}/`
923+
);
924+
const base64 = Buffer.from('user%:foo%zz', 'utf8').toString('base64');
925+
assert.strictEqual(response.data, `Basic ${base64}`);
926+
} finally {
927+
await stopHTTPServer(server);
928+
}
929+
});
930+
893931
it('should support basic auth with a header', async () => {
894932
const server = await startHTTPServer(
895933
(req, res) => {

0 commit comments

Comments
 (0)