Skip to content

Commit

Permalink
[fix] Abort the handshake if an unexpected extension is received
Browse files Browse the repository at this point in the history
Abort the handshake if the client receives a `Sec-WebSocket-Extensions`
header but no extension was requested. Also abort the handshake if the
server indicates an extension not requested by the client.
  • Loading branch information
lpinca committed Jul 3, 2021
1 parent 38c6c73 commit aca94c8
Show file tree
Hide file tree
Showing 2 changed files with 165 additions and 13 deletions.
52 changes: 40 additions & 12 deletions lib/websocket.js
Expand Up @@ -660,22 +660,50 @@ function initAsClient(websocket, address, protocols, options) {

if (serverProt) websocket._protocol = serverProt;

if (perMessageDeflate) {
const secWebSocketExtensions = res.headers['sec-websocket-extensions'];

if (secWebSocketExtensions !== undefined) {
if (!perMessageDeflate) {
const message =
'Server sent a Sec-WebSocket-Extensions header but no extension ' +
'was requested';
abortHandshake(websocket, socket, message);
return;
}

let extensions;

try {
const extensions = parse(res.headers['sec-websocket-extensions']);
extensions = parse(secWebSocketExtensions);
} catch (err) {
const message = 'Invalid Sec-WebSocket-Extensions header';
abortHandshake(websocket, socket, message);
return;
}

const extensionNames = Object.keys(extensions);

if (extensionNames.length) {
if (
extensionNames.length !== 1 ||
extensionNames[0] !== PerMessageDeflate.extensionName
) {
const message =
'Server indicated an extension that was not requested';
abortHandshake(websocket, socket, message);
return;
}

if (extensions[PerMessageDeflate.extensionName]) {
try {
perMessageDeflate.accept(extensions[PerMessageDeflate.extensionName]);
websocket._extensions[PerMessageDeflate.extensionName] =
perMessageDeflate;
} catch (err) {
const message = 'Invalid Sec-WebSocket-Extensions header';
abortHandshake(websocket, socket, message);
return;
}
} catch (err) {
abortHandshake(
websocket,
socket,
'Invalid Sec-WebSocket-Extensions header'
);
return;

websocket._extensions[PerMessageDeflate.extensionName] =
perMessageDeflate;
}
}

Expand Down
126 changes: 125 additions & 1 deletion test/websocket.test.js
Expand Up @@ -663,7 +663,40 @@ describe('WebSocket', () => {
});
});

it('fails if the Sec-WebSocket-Extensions response header is invalid', (done) => {
it('fails if an unexpected Sec-WebSocket-Extensions header is received', (done) => {
server.once('upgrade', (req, socket) => {
const key = crypto
.createHash('sha1')
.update(req.headers['sec-websocket-key'] + GUID)
.digest('base64');

socket.end(
'HTTP/1.1 101 Switching Protocols\r\n' +
'Upgrade: websocket\r\n' +
'Connection: Upgrade\r\n' +
`Sec-WebSocket-Accept: ${key}\r\n` +
'Sec-WebSocket-Extensions: foo\r\n' +
'\r\n'
);
});

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

ws.on('open', () => done(new Error("Unexpected 'open' event")));
ws.on('error', (err) => {
assert.ok(err instanceof Error);
assert.strictEqual(
err.message,
'Server sent a Sec-WebSocket-Extensions header but no extension ' +
'was requested'
);
ws.on('close', () => done());
});
});

it('fails if the Sec-WebSocket-Extensions header is invalid (1/2)', (done) => {
server.once('upgrade', (req, socket) => {
const key = crypto
.createHash('sha1')
Expand Down Expand Up @@ -693,6 +726,97 @@ describe('WebSocket', () => {
});
});

it('fails if the Sec-WebSocket-Extensions header is invalid (2/2)', (done) => {
server.once('upgrade', (req, socket) => {
const key = crypto
.createHash('sha1')
.update(req.headers['sec-websocket-key'] + GUID)
.digest('base64');

socket.end(
'HTTP/1.1 101 Switching Protocols\r\n' +
'Upgrade: websocket\r\n' +
'Connection: Upgrade\r\n' +
`Sec-WebSocket-Accept: ${key}\r\n` +
'Sec-WebSocket-Extensions: ' +
'permessage-deflate; client_max_window_bits=7\r\n' +
'\r\n'
);
});

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

ws.on('open', () => done(new Error("Unexpected 'open' event")));
ws.on('error', (err) => {
assert.ok(err instanceof Error);
assert.strictEqual(
err.message,
'Invalid Sec-WebSocket-Extensions header'
);
ws.on('close', () => done());
});
});

it('fails if an unexpected extension is received (1/2)', (done) => {
server.once('upgrade', (req, socket) => {
const key = crypto
.createHash('sha1')
.update(req.headers['sec-websocket-key'] + GUID)
.digest('base64');

socket.end(
'HTTP/1.1 101 Switching Protocols\r\n' +
'Upgrade: websocket\r\n' +
'Connection: Upgrade\r\n' +
`Sec-WebSocket-Accept: ${key}\r\n` +
'Sec-WebSocket-Extensions: foo\r\n' +
'\r\n'
);
});

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

ws.on('open', () => done(new Error("Unexpected 'open' event")));
ws.on('error', (err) => {
assert.ok(err instanceof Error);
assert.strictEqual(
err.message,
'Server indicated an extension that was not requested'
);
ws.on('close', () => done());
});
});

it('fails if an unexpected extension is received (2/2)', (done) => {
server.once('upgrade', (req, socket) => {
const key = crypto
.createHash('sha1')
.update(req.headers['sec-websocket-key'] + GUID)
.digest('base64');

socket.end(
'HTTP/1.1 101 Switching Protocols\r\n' +
'Upgrade: websocket\r\n' +
'Connection: Upgrade\r\n' +
`Sec-WebSocket-Accept: ${key}\r\n` +
'Sec-WebSocket-Extensions: permessage-deflate,foo\r\n' +
'\r\n'
);
});

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

ws.on('open', () => done(new Error("Unexpected 'open' event")));
ws.on('error', (err) => {
assert.ok(err instanceof Error);
assert.strictEqual(
err.message,
'Server indicated an extension that was not requested'
);
ws.on('close', () => done());
});
});

it('fails if server sends a subprotocol when none was requested', (done) => {
const wss = new WebSocket.Server({ server });

Expand Down

0 comments on commit aca94c8

Please sign in to comment.