Skip to content

Commit 1f72e2e

Browse files
committed
[security] Drop sensitive headers when following redirects (#2013)
Do not forward the `Authorization` and `Cookie` headers if the redirect host is different from the original host.
1 parent 8ecd890 commit 1f72e2e

File tree

2 files changed

+152
-0
lines changed

2 files changed

+152
-0
lines changed

lib/websocket.js

+39
Original file line numberDiff line numberDiff line change
@@ -682,6 +682,45 @@ function initAsClient(websocket, address, protocols, options) {
682682
opts.path = parts[1];
683683
}
684684

685+
if (opts.followRedirects) {
686+
if (websocket._redirects === 0) {
687+
websocket._originalHost = parsedUrl.host;
688+
689+
const headers = options && options.headers;
690+
691+
//
692+
// Shallow copy the user provided options so that headers can be changed
693+
// without mutating the original object.
694+
//
695+
options = { ...options, headers: {} };
696+
697+
if (headers) {
698+
for (const [key, value] of Object.entries(headers)) {
699+
options.headers[key.toLowerCase()] = value;
700+
}
701+
}
702+
} else if (parsedUrl.host !== websocket._originalHost) {
703+
//
704+
// Match curl 7.77.0 behavior and drop the following headers. These
705+
// headers are also dropped when following a redirect to a subdomain.
706+
//
707+
delete opts.headers.authorization;
708+
delete opts.headers.cookie;
709+
delete opts.headers.host;
710+
opts.auth = undefined;
711+
}
712+
713+
//
714+
// Match curl 7.77.0 behavior and make the first `Authorization` header win.
715+
// If the `Authorization` header is set, then there is nothing to do as it
716+
// will take precedence.
717+
//
718+
if (opts.auth && !options.headers.authorization) {
719+
options.headers.authorization =
720+
'Basic ' + Buffer.from(opts.auth).toString('base64');
721+
}
722+
}
723+
685724
let req = (websocket._req = get(opts));
686725

687726
if (opts.timeout) {

test/websocket.test.js

+113
Original file line numberDiff line numberDiff line change
@@ -986,6 +986,119 @@ describe('WebSocket', () => {
986986
ws.on('close', () => done());
987987
});
988988
});
989+
990+
it('uses the first url userinfo when following redirects', (done) => {
991+
const wss = new WebSocket.Server({ noServer: true, path: '/foo' });
992+
const authorization = 'Basic Zm9vOmJhcg==';
993+
994+
server.once('upgrade', (req, socket) => {
995+
socket.end('HTTP/1.1 302 Found\r\nLocation: /foo\r\n\r\n');
996+
server.once('upgrade', (req, socket, head) => {
997+
wss.handleUpgrade(req, socket, head, (ws, req) => {
998+
assert.strictEqual(req.headers.authorization, authorization);
999+
ws.close();
1000+
});
1001+
});
1002+
});
1003+
1004+
const port = server.address().port;
1005+
const ws = new WebSocket(`ws://foo:bar@localhost:${port}`, {
1006+
followRedirects: true
1007+
});
1008+
1009+
assert.strictEqual(ws._req.getHeader('Authorization'), authorization);
1010+
1011+
ws.on('close', (code) => {
1012+
assert.strictEqual(code, 1005);
1013+
assert.strictEqual(ws.url, `ws://foo:bar@localhost:${port}/foo`);
1014+
assert.strictEqual(ws._redirects, 1);
1015+
1016+
wss.close(done);
1017+
});
1018+
});
1019+
1020+
describe('When the redirect host is different', () => {
1021+
it('drops the `auth` option', (done) => {
1022+
const wss = new WebSocket.Server({ port: 0 }, () => {
1023+
const port = wss.address().port;
1024+
1025+
server.once('upgrade', (req, socket) => {
1026+
socket.end(
1027+
`HTTP/1.1 302 Found\r\nLocation: ws://localhost:${port}/\r\n\r\n`
1028+
);
1029+
});
1030+
1031+
const ws = new WebSocket(`ws://localhost:${server.address().port}`, {
1032+
auth: 'foo:bar',
1033+
followRedirects: true
1034+
});
1035+
1036+
assert.strictEqual(
1037+
ws._req.getHeader('Authorization'),
1038+
'Basic Zm9vOmJhcg=='
1039+
);
1040+
1041+
ws.on('close', (code) => {
1042+
assert.strictEqual(code, 1005);
1043+
assert.strictEqual(ws.url, `ws://localhost:${port}/`);
1044+
assert.strictEqual(ws._redirects, 1);
1045+
1046+
wss.close(done);
1047+
});
1048+
});
1049+
1050+
wss.on('connection', (ws, req) => {
1051+
assert.strictEqual(req.headers.authorization, undefined);
1052+
ws.close();
1053+
});
1054+
});
1055+
1056+
it('drops the Authorization, Cookie, and Host headers', (done) => {
1057+
const wss = new WebSocket.Server({ port: 0 }, () => {
1058+
const port = wss.address().port;
1059+
1060+
server.once('upgrade', (req, socket) => {
1061+
socket.end(
1062+
`HTTP/1.1 302 Found\r\nLocation: ws://localhost:${port}/\r\n\r\n`
1063+
);
1064+
});
1065+
1066+
const ws = new WebSocket(`ws://localhost:${server.address().port}`, {
1067+
headers: {
1068+
Authorization: 'Basic Zm9vOmJhcg==',
1069+
Cookie: 'foo=bar',
1070+
Host: 'foo'
1071+
},
1072+
followRedirects: true
1073+
});
1074+
1075+
assert.strictEqual(
1076+
ws._req.getHeader('Authorization'),
1077+
'Basic Zm9vOmJhcg=='
1078+
);
1079+
assert.strictEqual(ws._req.getHeader('Cookie'), 'foo=bar');
1080+
assert.strictEqual(ws._req.getHeader('Host'), 'foo');
1081+
1082+
ws.on('close', (code) => {
1083+
assert.strictEqual(code, 1005);
1084+
assert.strictEqual(ws.url, `ws://localhost:${port}/`);
1085+
assert.strictEqual(ws._redirects, 1);
1086+
1087+
wss.close(done);
1088+
});
1089+
});
1090+
1091+
wss.on('connection', (ws, req) => {
1092+
assert.strictEqual(req.headers.authorization, undefined);
1093+
assert.strictEqual(req.headers.cookie, undefined);
1094+
assert.strictEqual(
1095+
req.headers.host,
1096+
`localhost:${wss.address().port}`
1097+
);
1098+
ws.close();
1099+
});
1100+
});
1101+
});
9891102
});
9901103

9911104
describe('Connection with query string', () => {

0 commit comments

Comments
 (0)