Skip to content

Commit

Permalink
[security] Fix same host check for ws+unix: redirects
Browse files Browse the repository at this point in the history
Drop the `Authorization` and `Cookie` headers if the original request
for the opening handshake is sent to an IPC server and the client is
redirected to a TCP server (ws+unix: to ws: or wss:), and vice versa
(ws: or wss: to ws+unix).

Also drop the `Authorization` and `Cookie` headers if the original
request for the opening handshake is sent to an IPC server and the
client is redirected to another IPC server.

Refs: 6946f5fe
  • Loading branch information
lpinca committed Jul 15, 2022
1 parent 0ae302a commit bc8bd34
Show file tree
Hide file tree
Showing 2 changed files with 231 additions and 3 deletions.
13 changes: 11 additions & 2 deletions lib/websocket.js
Original file line number Diff line number Diff line change
Expand Up @@ -771,8 +771,11 @@ function initAsClient(websocket, address, protocols, options) {

if (opts.followRedirects) {
if (websocket._redirects === 0) {
websocket._originalUnixSocket = isUnixSocket;
websocket._originalSecure = isSecure;
websocket._originalHost = parsedUrl.host;
websocket._originalHostOrSocketPath = isUnixSocket
? opts.socketPath
: parsedUrl.host;

const headers = options && options.headers;

Expand All @@ -788,7 +791,13 @@ function initAsClient(websocket, address, protocols, options) {
}
}
} else if (websocket.listenerCount('redirect') === 0) {
const isSameHost = parsedUrl.host === websocket._originalHost;
const isSameHost = isUnixSocket
? websocket._originalUnixSocket
? opts.socketPath === websocket._originalHostOrSocketPath
: false
: websocket._originalUnixSocket
? false
: parsedUrl.host === websocket._originalHostOrSocketPath;

if (!isSameHost || (websocket._originalSecure && !isSecure)) {
//
Expand Down
221 changes: 220 additions & 1 deletion test/websocket.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ const assert = require('assert');
const crypto = require('crypto');
const https = require('https');
const http = require('http');
const path = require('path');
const net = require('net');
const tls = require('tls');
const os = require('os');
const fs = require('fs');
const { URL } = require('url');

Expand Down Expand Up @@ -1477,7 +1479,9 @@ describe('WebSocket', () => {
});
});

it('drops the Authorization, Cookie and Host headers', (done) => {
it('drops the Authorization, Cookie and Host headers (1/4)', (done) => {
// Test the `ws:` to `ws:` case.

const wss = new WebSocket.Server({ port: 0 }, () => {
const port = wss.address().port;

Expand Down Expand Up @@ -1531,6 +1535,221 @@ describe('WebSocket', () => {
ws.close();
});
});

it('drops the Authorization, Cookie and Host headers (2/4)', function (done) {
if (process.platform === 'win32') return this.skip();

// Test the `ws:` to `ws+unix:` case.

const socketPath = path.join(
os.tmpdir(),
`ws.${crypto.randomBytes(16).toString('hex')}.sock`
);

server.once('upgrade', (req, socket) => {
socket.end(
`HTTP/1.1 302 Found\r\nLocation: ws+unix://${socketPath}\r\n\r\n`
);
});

const redirectedServer = http.createServer();
const wss = new WebSocket.Server({ server: redirectedServer });

wss.on('connection', (ws, req) => {
assert.strictEqual(req.headers.authorization, undefined);
assert.strictEqual(req.headers.cookie, undefined);
assert.strictEqual(req.headers.host, 'localhost');

ws.close();
});

redirectedServer.listen(socketPath, () => {
const headers = {
authorization: 'Basic Zm9vOmJhcg==',
cookie: 'foo=bar',
host: 'foo'
};

const ws = new WebSocket(
`ws://localhost:${server.address().port}`,
{ followRedirects: true, headers }
);

const firstRequest = ws._req;

assert.strictEqual(
firstRequest.getHeader('Authorization'),
headers.authorization
);
assert.strictEqual(
firstRequest.getHeader('Cookie'),
headers.cookie
);
assert.strictEqual(firstRequest.getHeader('Host'), headers.host);

ws.on('close', (code) => {
assert.strictEqual(code, 1005);
assert.strictEqual(ws.url, `ws+unix://${socketPath}`);
assert.strictEqual(ws._redirects, 1);

redirectedServer.close(done);
});
});
});

it('drops the Authorization, Cookie and Host headers (3/4)', function (done) {
if (process.platform === 'win32') return this.skip();

// Test the `ws+unix:` to `ws+unix:` case.

const redirectingServerSocketPath = path.join(
os.tmpdir(),
`ws.${crypto.randomBytes(16).toString('hex')}.sock`
);
const redirectedServerSocketPath = path.join(
os.tmpdir(),
`ws.${crypto.randomBytes(16).toString('hex')}.sock`
);

const redirectingServer = http.createServer();

redirectingServer.on('upgrade', (req, socket) => {
socket.end(
'HTTP/1.1 302 Found\r\n' +
`Location: ws+unix://${redirectedServerSocketPath}\r\n\r\n`
);
});

const redirectedServer = http.createServer();
const wss = new WebSocket.Server({ server: redirectedServer });

wss.on('connection', (ws, req) => {
assert.strictEqual(req.headers.authorization, undefined);
assert.strictEqual(req.headers.cookie, undefined);
assert.strictEqual(req.headers.host, 'localhost');

ws.close();
});

redirectingServer.listen(redirectingServerSocketPath, listening);
redirectedServer.listen(redirectedServerSocketPath, listening);

let callCount = 0;

function listening() {
if (++callCount !== 2) return;

const headers = {
authorization: 'Basic Zm9vOmJhcg==',
cookie: 'foo=bar',
host: 'foo'
};

const ws = new WebSocket(
`ws+unix://${redirectingServerSocketPath}`,
{ followRedirects: true, headers }
);

const firstRequest = ws._req;

assert.strictEqual(
firstRequest.getHeader('Authorization'),
headers.authorization
);
assert.strictEqual(
firstRequest.getHeader('Cookie'),
headers.cookie
);
assert.strictEqual(firstRequest.getHeader('Host'), headers.host);

ws.on('close', (code) => {
assert.strictEqual(code, 1005);
assert.strictEqual(
ws.url,
`ws+unix://${redirectedServerSocketPath}`
);
assert.strictEqual(ws._redirects, 1);

redirectingServer.close();
redirectedServer.close(done);
});
}
});

it('drops the Authorization, Cookie and Host headers (4/4)', function (done) {
if (process.platform === 'win32') return this.skip();

// Test the `ws+unix:` to `ws:` case.

const redirectingServer = http.createServer();
const redirectedServer = http.createServer();
const wss = new WebSocket.Server({ server: redirectedServer });

wss.on('connection', (ws, req) => {
assert.strictEqual(req.headers.authorization, undefined);
assert.strictEqual(req.headers.cookie, undefined);
assert.strictEqual(
req.headers.host,
`localhost:${redirectedServer.address().port}`
);

ws.close();
});

const socketPath = path.join(
os.tmpdir(),
`ws.${crypto.randomBytes(16).toString('hex')}.sock`
);

redirectingServer.listen(socketPath, listening);
redirectedServer.listen(0, listening);

let callCount = 0;

function listening() {
if (++callCount !== 2) return;

const port = redirectedServer.address().port;

redirectingServer.on('upgrade', (req, socket) => {
socket.end(
`HTTP/1.1 302 Found\r\nLocation: ws://localhost:${port}\r\n\r\n`
);
});

const headers = {
authorization: 'Basic Zm9vOmJhcg==',
cookie: 'foo=bar',
host: 'foo'
};

const ws = new WebSocket(`ws+unix://${socketPath}`, {
followRedirects: true,
headers
});

const firstRequest = ws._req;

assert.strictEqual(
firstRequest.getHeader('Authorization'),
headers.authorization
);
assert.strictEqual(
firstRequest.getHeader('Cookie'),
headers.cookie
);
assert.strictEqual(firstRequest.getHeader('Host'), headers.host);

ws.on('close', (code) => {
assert.strictEqual(code, 1005);
assert.strictEqual(ws.url, `ws://localhost:${port}/`);
assert.strictEqual(ws._redirects, 1);

redirectingServer.close();
redirectedServer.close(done);
});
}
});
});

describe("If there is at least one 'redirect' event listener", () => {
Expand Down

0 comments on commit bc8bd34

Please sign in to comment.