Skip to content

Commit 6946f5f

Browse files
authored
[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 75fdfa9 commit 6946f5f

File tree

2 files changed

+152
-0
lines changed

2 files changed

+152
-0
lines changed

Diff for: lib/websocket.js

+39
Original file line numberDiff line numberDiff line change
@@ -766,6 +766,45 @@ function initAsClient(websocket, address, protocols, options) {
766766
opts.path = parts[1];
767767
}
768768

769+
if (opts.followRedirects) {
770+
if (websocket._redirects === 0) {
771+
websocket._originalHost = parsedUrl.host;
772+
773+
const headers = options && options.headers;
774+
775+
//
776+
// Shallow copy the user provided options so that headers can be changed
777+
// without mutating the original object.
778+
//
779+
options = { ...options, headers: {} };
780+
781+
if (headers) {
782+
for (const [key, value] of Object.entries(headers)) {
783+
options.headers[key.toLowerCase()] = value;
784+
}
785+
}
786+
} else if (parsedUrl.host !== websocket._originalHost) {
787+
//
788+
// Match curl 7.77.0 behavior and drop the following headers. These
789+
// headers are also dropped when following a redirect to a subdomain.
790+
//
791+
delete opts.headers.authorization;
792+
delete opts.headers.cookie;
793+
delete opts.headers.host;
794+
opts.auth = undefined;
795+
}
796+
797+
//
798+
// Match curl 7.77.0 behavior and make the first `Authorization` header win.
799+
// If the `Authorization` header is set, then there is nothing to do as it
800+
// will take precedence.
801+
//
802+
if (opts.auth && !options.headers.authorization) {
803+
options.headers.authorization =
804+
'Basic ' + Buffer.from(opts.auth).toString('base64');
805+
}
806+
}
807+
769808
let req = (websocket._req = get(opts));
770809

771810
if (opts.timeout) {

Diff for: test/websocket.test.js

+113
Original file line numberDiff line numberDiff line change
@@ -1140,6 +1140,119 @@ describe('WebSocket', () => {
11401140
ws.on('close', () => done());
11411141
});
11421142
});
1143+
1144+
it('uses the first url userinfo when following redirects', (done) => {
1145+
const wss = new WebSocket.Server({ noServer: true, path: '/foo' });
1146+
const authorization = 'Basic Zm9vOmJhcg==';
1147+
1148+
server.once('upgrade', (req, socket) => {
1149+
socket.end('HTTP/1.1 302 Found\r\nLocation: /foo\r\n\r\n');
1150+
server.once('upgrade', (req, socket, head) => {
1151+
wss.handleUpgrade(req, socket, head, (ws, req) => {
1152+
assert.strictEqual(req.headers.authorization, authorization);
1153+
ws.close();
1154+
});
1155+
});
1156+
});
1157+
1158+
const port = server.address().port;
1159+
const ws = new WebSocket(`ws://foo:bar@localhost:${port}`, {
1160+
followRedirects: true
1161+
});
1162+
1163+
assert.strictEqual(ws._req.getHeader('Authorization'), authorization);
1164+
1165+
ws.on('close', (code) => {
1166+
assert.strictEqual(code, 1005);
1167+
assert.strictEqual(ws.url, `ws://foo:bar@localhost:${port}/foo`);
1168+
assert.strictEqual(ws._redirects, 1);
1169+
1170+
wss.close(done);
1171+
});
1172+
});
1173+
1174+
describe('When the redirect host is different', () => {
1175+
it('drops the `auth` option', (done) => {
1176+
const wss = new WebSocket.Server({ port: 0 }, () => {
1177+
const port = wss.address().port;
1178+
1179+
server.once('upgrade', (req, socket) => {
1180+
socket.end(
1181+
`HTTP/1.1 302 Found\r\nLocation: ws://localhost:${port}/\r\n\r\n`
1182+
);
1183+
});
1184+
1185+
const ws = new WebSocket(`ws://localhost:${server.address().port}`, {
1186+
auth: 'foo:bar',
1187+
followRedirects: true
1188+
});
1189+
1190+
assert.strictEqual(
1191+
ws._req.getHeader('Authorization'),
1192+
'Basic Zm9vOmJhcg=='
1193+
);
1194+
1195+
ws.on('close', (code) => {
1196+
assert.strictEqual(code, 1005);
1197+
assert.strictEqual(ws.url, `ws://localhost:${port}/`);
1198+
assert.strictEqual(ws._redirects, 1);
1199+
1200+
wss.close(done);
1201+
});
1202+
});
1203+
1204+
wss.on('connection', (ws, req) => {
1205+
assert.strictEqual(req.headers.authorization, undefined);
1206+
ws.close();
1207+
});
1208+
});
1209+
1210+
it('drops the Authorization, Cookie, and Host headers', (done) => {
1211+
const wss = new WebSocket.Server({ port: 0 }, () => {
1212+
const port = wss.address().port;
1213+
1214+
server.once('upgrade', (req, socket) => {
1215+
socket.end(
1216+
`HTTP/1.1 302 Found\r\nLocation: ws://localhost:${port}/\r\n\r\n`
1217+
);
1218+
});
1219+
1220+
const ws = new WebSocket(`ws://localhost:${server.address().port}`, {
1221+
headers: {
1222+
Authorization: 'Basic Zm9vOmJhcg==',
1223+
Cookie: 'foo=bar',
1224+
Host: 'foo'
1225+
},
1226+
followRedirects: true
1227+
});
1228+
1229+
assert.strictEqual(
1230+
ws._req.getHeader('Authorization'),
1231+
'Basic Zm9vOmJhcg=='
1232+
);
1233+
assert.strictEqual(ws._req.getHeader('Cookie'), 'foo=bar');
1234+
assert.strictEqual(ws._req.getHeader('Host'), 'foo');
1235+
1236+
ws.on('close', (code) => {
1237+
assert.strictEqual(code, 1005);
1238+
assert.strictEqual(ws.url, `ws://localhost:${port}/`);
1239+
assert.strictEqual(ws._redirects, 1);
1240+
1241+
wss.close(done);
1242+
});
1243+
});
1244+
1245+
wss.on('connection', (ws, req) => {
1246+
assert.strictEqual(req.headers.authorization, undefined);
1247+
assert.strictEqual(req.headers.cookie, undefined);
1248+
assert.strictEqual(
1249+
req.headers.host,
1250+
`localhost:${wss.address().port}`
1251+
);
1252+
ws.close();
1253+
});
1254+
});
1255+
});
11431256
});
11441257

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

0 commit comments

Comments
 (0)