Skip to content

Commit b7089ff

Browse files
committed
[fix] Rewrite the parser of the Sec-WebSocket-Extensions header
Make the parser correctly handle quoted values and close the connection if the `Sec-WebSocket-Extensions` header is invalid. Fixes #1235
1 parent d96c58c commit b7089ff

File tree

5 files changed

+301
-65
lines changed

5 files changed

+301
-65
lines changed

lib/Extensions.js

Lines changed: 174 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,203 @@
11
'use strict';
22

3+
//
4+
// Allowed token characters:
5+
//
6+
// '!', '#', '$', '%', '&', ''', '*', '+', '-',
7+
// '.', 0-9, A-Z, '^', '_', '`', a-z, '|', '~'
8+
//
9+
// tokenChars[32] === 0 // ' '
10+
// tokenChars[33] === 1 // '!'
11+
// tokenChars[34] === 0 // '"'
12+
// ...
13+
//
14+
const tokenChars = [
15+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0 - 15
16+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16 - 31
17+
0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, // 32 - 47
18+
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, // 48 - 63
19+
0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 64 - 79
20+
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, // 80 - 95
21+
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 96 - 111
22+
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0 // 112 - 127
23+
];
24+
325
/**
4-
* Parse the `Sec-WebSocket-Extensions` header into an object.
26+
* Adds an offer to the map of extension offers.
527
*
6-
* @param {String} value field value of the header
28+
* @param {Object} offers The map of extension offers
29+
* @param {String} name The extension name
30+
* @param {Object} params The extension parameters
31+
* @private
32+
*/
33+
function pushOffer (offers, name, params) {
34+
if (Object.hasOwnProperty.call(offers, name)) offers[name].push(params);
35+
else offers[name] = [params];
36+
}
37+
38+
/**
39+
* Adds a parameter to the map of parameters.
40+
*
41+
* @param {Object} params The map of parameters
42+
* @param {String} name The parameter name
43+
* @param {Object} value The parameter value
44+
* @private
45+
*/
46+
function pushParam (params, name, value) {
47+
if (Object.hasOwnProperty.call(params, name)) params[name].push(value);
48+
else params[name] = [value];
49+
}
50+
51+
/**
52+
* Parses the `Sec-WebSocket-Extensions` header into an object.
53+
*
54+
* @param {String} header The field value of the header
755
* @return {Object} The parsed object
856
* @public
957
*/
10-
const parse = (value) => {
11-
value = value || '';
58+
function parse (header) {
59+
const offers = {};
1260

13-
const extensions = {};
61+
if (header === undefined || header === '') return offers;
1462

15-
value.split(',').forEach((v) => {
16-
const params = v.split(';');
17-
const token = params.shift().trim();
63+
var params = {};
64+
var mustUnescape = false;
65+
var isEscaping = false;
66+
var inQuotes = false;
67+
var extensionName;
68+
var paramName;
69+
var start = -1;
70+
var end = -1;
1871

19-
if (extensions[token] === undefined) {
20-
extensions[token] = [];
21-
} else if (!extensions.hasOwnProperty(token)) {
22-
return;
23-
}
72+
for (var i = 0; i < header.length; i++) {
73+
const code = header.charCodeAt(i);
2474

25-
const parsedParams = {};
75+
if (extensionName === undefined) {
76+
if (end === -1 && tokenChars[code] === 1) {
77+
if (start === -1) start = i;
78+
} else if (code === 0x20/* ' ' */|| code === 0x09/* '\t' */) {
79+
if (end === -1 && start !== -1) end = i;
80+
} else if (code === 0x3b/* ';' */ || code === 0x2c/* ',' */) {
81+
if (start === -1) throw new Error(`unexpected character at index ${i}`);
2682

27-
params.forEach((param) => {
28-
const parts = param.trim().split('=');
29-
const key = parts[0];
30-
var value = parts[1];
83+
if (end === -1) end = i;
84+
const name = header.slice(start, end);
85+
if (code === 0x2c) {
86+
pushOffer(offers, name, params);
87+
params = {};
88+
} else {
89+
extensionName = name;
90+
}
3191

32-
if (value === undefined) {
33-
value = true;
92+
start = end = -1;
3493
} else {
35-
// unquote value
36-
if (value[0] === '"') {
37-
value = value.slice(1);
38-
}
39-
if (value[value.length - 1] === '"') {
40-
value = value.slice(0, value.length - 1);
94+
throw new Error(`unexpected character at index ${i}`);
95+
}
96+
} else if (paramName === undefined) {
97+
if (end === -1 && tokenChars[code] === 1) {
98+
if (start === -1) start = i;
99+
} else if (code === 0x20 || code === 0x09) {
100+
if (end === -1 && start !== -1) end = i;
101+
} else if (code === 0x3b || code === 0x2c) {
102+
if (start === -1) throw new Error(`unexpected character at index ${i}`);
103+
104+
if (end === -1) end = i;
105+
pushParam(params, header.slice(start, end), true);
106+
if (code === 0x2c) {
107+
pushOffer(offers, extensionName, params);
108+
params = {};
109+
extensionName = undefined;
41110
}
111+
112+
start = end = -1;
113+
} else if (code === 0x3d/* '=' */&& start !== -1 && end === -1) {
114+
paramName = header.slice(start, i);
115+
start = end = -1;
116+
} else {
117+
throw new Error(`unexpected character at index ${i}`);
42118
}
119+
} else {
120+
//
121+
// The value of a quoted-string after unescaping must conform to the
122+
// token ABNF, so only token characters are valid.
123+
// Ref: https://tools.ietf.org/html/rfc6455#section-9.1
124+
//
125+
if (isEscaping) {
126+
if (tokenChars[code] !== 1) {
127+
throw new Error(`unexpected character at index ${i}`);
128+
}
129+
if (start === -1) start = i;
130+
else if (!mustUnescape) mustUnescape = true;
131+
isEscaping = false;
132+
} else if (inQuotes) {
133+
if (tokenChars[code] === 1) {
134+
if (start === -1) start = i;
135+
} else if (code === 0x22/* '"' */ && start !== -1) {
136+
inQuotes = false;
137+
end = i;
138+
} else if (code === 0x5c/* '\' */) {
139+
isEscaping = true;
140+
} else {
141+
throw new Error(`unexpected character at index ${i}`);
142+
}
143+
} else if (code === 0x22 && header.charCodeAt(i - 1) === 0x3d) {
144+
inQuotes = true;
145+
} else if (end === -1 && tokenChars[code] === 1) {
146+
if (start === -1) start = i;
147+
} else if (start !== -1 && (code === 0x20 || code === 0x09)) {
148+
if (end === -1) end = i;
149+
} else if (code === 0x3b || code === 0x2c) {
150+
if (start === -1) throw new Error(`unexpected character at index ${i}`);
151+
152+
if (end === -1) end = i;
153+
var value = header.slice(start, end);
154+
if (mustUnescape) {
155+
value = value.replace(/\\/g, '');
156+
mustUnescape = false;
157+
}
158+
pushParam(params, paramName, value);
159+
if (code === 0x2c) {
160+
pushOffer(offers, extensionName, params);
161+
params = {};
162+
extensionName = undefined;
163+
}
43164

44-
if (parsedParams[key] === undefined) {
45-
parsedParams[key] = [value];
46-
} else if (parsedParams.hasOwnProperty(key)) {
47-
parsedParams[key].push(value);
165+
paramName = undefined;
166+
start = end = -1;
167+
} else {
168+
throw new Error(`unexpected character at index ${i}`);
48169
}
49-
});
170+
}
171+
}
172+
173+
if (start === -1 || inQuotes) throw new Error('unexpected end of input');
50174

51-
extensions[token].push(parsedParams);
52-
});
175+
if (end === -1) end = i;
176+
const token = header.slice(start, end);
177+
if (extensionName === undefined) {
178+
pushOffer(offers, token, {});
179+
} else {
180+
if (paramName === undefined) {
181+
pushParam(params, token, true);
182+
} else if (mustUnescape) {
183+
pushParam(params, paramName, token.replace(/\\/g, ''));
184+
} else {
185+
pushParam(params, paramName, token);
186+
}
187+
pushOffer(offers, extensionName, params);
188+
}
53189

54-
return extensions;
55-
};
190+
return offers;
191+
}
56192

57193
/**
58-
* Serialize a parsed `Sec-WebSocket-Extensions` header to a string.
194+
* Serializes a parsed `Sec-WebSocket-Extensions` header to a string.
59195
*
60196
* @param {Object} value The object to format
61197
* @return {String} A string representing the given value
62198
* @public
63199
*/
64-
const format = (value) => {
200+
function format (value) {
65201
return Object.keys(value).map((token) => {
66202
var paramsList = value[token];
67203
if (!Array.isArray(paramsList)) paramsList = [paramsList];
@@ -73,6 +209,6 @@ const format = (value) => {
73209
})).join('; ');
74210
}).join(', ');
75211
}).join(', ');
76-
};
212+
}
77213

78214
module.exports = { format, parse };

lib/WebSocket.js

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -692,18 +692,17 @@ function initAsClient (address, protocols, options) {
692692

693693
if (serverProt) this.protocol = serverProt;
694694

695-
const serverExtensions = Extensions.parse(res.headers['sec-websocket-extensions']);
695+
try {
696+
const serverExtensions = Extensions.parse(res.headers['sec-websocket-extensions']);
696697

697-
if (perMessageDeflate && serverExtensions[PerMessageDeflate.extensionName]) {
698-
try {
698+
if (perMessageDeflate && serverExtensions[PerMessageDeflate.extensionName]) {
699699
perMessageDeflate.accept(serverExtensions[PerMessageDeflate.extensionName]);
700-
} catch (err) {
701-
socket.destroy();
702-
this.emit('error', new Error('invalid extension parameter'));
703-
return this.finalize(true);
700+
this.extensions[PerMessageDeflate.extensionName] = perMessageDeflate;
704701
}
705-
706-
this.extensions[PerMessageDeflate.extensionName] = perMessageDeflate;
702+
} catch (err) {
703+
socket.destroy();
704+
this.emit('error', new Error('invalid Sec-WebSocket-Extensions header'));
705+
return this.finalize(true);
707706
}
708707

709708
this.setSocket(socket, head);

lib/WebSocketServer.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -227,11 +227,11 @@ class WebSocketServer extends EventEmitter {
227227

228228
if (protocol) headers.push(`Sec-WebSocket-Protocol: ${protocol}`);
229229

230-
const offer = Extensions.parse(req.headers['sec-websocket-extensions']);
231230
var extensions;
232231

233232
try {
234-
extensions = acceptExtensions(this.options, offer);
233+
const offers = Extensions.parse(req.headers['sec-websocket-extensions']);
234+
extensions = acceptExtensions(this.options, offers);
235235
} catch (err) {
236236
return abortConnection(socket, 400);
237237
}

0 commit comments

Comments
 (0)