Comparison

plugins/mod_websocket.lua @ 11113:10301c214f4e 0.11

mod_websocket: Refactor frame validity checking, also check partially-received frames against constraints
author Matthew Wild <mwild1@gmail.com>
date Tue, 29 Sep 2020 15:18:32 +0100
parent 11111:55d8612ac357
child 11114:6a608ecb3471
child 11540:1937b3c3efb5
comparison
equal deleted inserted replaced
11112:bcc701377fe4 11113:10301c214f4e
140 return oc:top_tag(); 140 return oc:top_tag();
141 end 141 end
142 142
143 return data; 143 return data;
144 end 144 end
145
146 local function validate_frame(frame, max_length)
147 local opcode, length = frame.opcode, frame.length;
148
149 if max_length and length > max_length then
150 return false, 1009, "Payload too large";
151 end
152
153 -- Error cases
154 if frame.RSV1 or frame.RSV2 or frame.RSV3 then -- Reserved bits non zero
155 return false, 1002, "Reserved bits not zero";
156 end
157
158 if opcode == 0x8 and frame.data then -- close frame
159 if length == 1 then
160 return false, 1002, "Close frame with payload, but too short for status code";
161 elseif length >= 2 then
162 local status_code = parse_close(frame.data)
163 if status_code < 1000 then
164 return false, 1002, "Closed with invalid status code";
165 elseif ((status_code > 1003 and status_code < 1007) or status_code > 1011) and status_code < 3000 then
166 return false, 1002, "Closed with reserved status code";
167 end
168 end
169 end
170
171 if opcode >= 0x8 then
172 if length > 125 then -- Control frame with too much payload
173 return false, 1002, "Payload too large";
174 end
175
176 if not frame.FIN then -- Fragmented control frame
177 return false, 1002, "Fragmented control frame";
178 end
179 end
180
181 if (opcode > 0x2 and opcode < 0x8) or (opcode > 0xA) then
182 return false, 1002, "Reserved opcode";
183 end
184
185 -- Check opcode
186 if opcode == 0x2 then -- Binary frame
187 return false, 1003, "Only text frames are supported, RFC 7395 3.2";
188 elseif opcode == 0x8 then -- Close request
189 return false, 1000, "Goodbye";
190 end
191
192 -- Other (XMPP-specific) validity checks
193 if not frame.FIN then
194 return false, 1003, "Continuation frames are not supported, RFC 7395 3.3.3";
195 end
196 if opcode == 0x01 and frame.data and frame.data:byte(1, 1) ~= 60 then
197 return false, 1007, "Invalid payload start character, RFC 7395 3.3.3";
198 end
199
200 return true;
201 end
202
203
145 function handle_request(event) 204 function handle_request(event)
146 local request, response = event.request, event.response; 205 local request, response = event.request, event.response;
147 local conn = response.conn; 206 local conn = response.conn;
148 207
149 conn.starttls = false; -- Prevent mod_tls from believing starttls can be done 208 conn.starttls = false; -- Prevent mod_tls from believing starttls can be done
170 local function websocket_close(code, message) 229 local function websocket_close(code, message)
171 conn:write(build_close(code, message)); 230 conn:write(build_close(code, message));
172 conn:close(); 231 conn:close();
173 end 232 end
174 233
175 local dataBuffer; 234 local function websocket_handle_error(session, code, message)
235 if code == 1009 then -- stanza size limit exceeded
236 -- we close the session, rather than the connection,
237 -- otherwise a resuming client will simply resend the
238 -- offending stanza
239 session:close({ condition = "policy-violation", text = "stanza too large" });
240 else
241 websocket_close(code, message);
242 end
243 end
244
176 local function handle_frame(frame) 245 local function handle_frame(frame)
246 module:log("debug", "Websocket received frame: opcode=%0x, %i bytes", frame.opcode, #frame.data);
247
248 -- Check frame makes sense
249 local frame_ok, err_status, err_text = validate_frame(frame, stanza_size_limit);
250 if not frame_ok then
251 return frame_ok, err_status, err_text;
252 end
253
177 local opcode = frame.opcode; 254 local opcode = frame.opcode;
178 local length = frame.length; 255 if opcode == 0x9 then -- Ping frame
179 module:log("debug", "Websocket received frame: opcode=%0x, %i bytes", frame.opcode, #frame.data);
180
181 -- Error cases
182 if frame.RSV1 or frame.RSV2 or frame.RSV3 then -- Reserved bits non zero
183 websocket_close(1002, "Reserved bits not zero");
184 return false;
185 end
186
187 if opcode == 0x8 then -- close frame
188 if length == 1 then
189 websocket_close(1002, "Close frame with payload, but too short for status code");
190 return false;
191 elseif length >= 2 then
192 local status_code = parse_close(frame.data)
193 if status_code < 1000 then
194 websocket_close(1002, "Closed with invalid status code");
195 return false;
196 elseif ((status_code > 1003 and status_code < 1007) or status_code > 1011) and status_code < 3000 then
197 websocket_close(1002, "Closed with reserved status code");
198 return false;
199 end
200 end
201 end
202
203 if opcode >= 0x8 then
204 if length > 125 then -- Control frame with too much payload
205 websocket_close(1002, "Payload too large");
206 return false;
207 end
208
209 if not frame.FIN then -- Fragmented control frame
210 websocket_close(1002, "Fragmented control frame");
211 return false;
212 end
213 end
214
215 if (opcode > 0x2 and opcode < 0x8) or (opcode > 0xA) then
216 websocket_close(1002, "Reserved opcode");
217 return false;
218 end
219
220 if opcode == 0x0 and not dataBuffer then
221 websocket_close(1002, "Unexpected continuation frame");
222 return false;
223 end
224
225 if (opcode == 0x1 or opcode == 0x2) and dataBuffer then
226 websocket_close(1002, "Continuation frame expected");
227 return false;
228 end
229
230 -- Valid cases
231 if opcode == 0x0 then -- Continuation frame
232 dataBuffer[#dataBuffer+1] = frame.data;
233 elseif opcode == 0x1 then -- Text frame
234 dataBuffer = {frame.data};
235 elseif opcode == 0x2 then -- Binary frame
236 websocket_close(1003, "Only text frames are supported");
237 return;
238 elseif opcode == 0x8 then -- Close request
239 websocket_close(1000, "Goodbye");
240 return;
241 elseif opcode == 0x9 then -- Ping frame
242 frame.opcode = 0xA; 256 frame.opcode = 0xA;
243 frame.MASK = false; -- Clients send masked frames, servers don't, see #1484 257 frame.MASK = false; -- Clients send masked frames, servers don't, see #1484
244 conn:write(build_frame(frame)); 258 conn:write(build_frame(frame));
245 return ""; 259 return "";
246 elseif opcode == 0xA then -- Pong frame, MAY be sent unsolicited, eg as keepalive 260 elseif opcode == 0xA then -- Pong frame, MAY be sent unsolicited, eg as keepalive
247 return ""; 261 return "";
248 else 262 elseif opcode ~= 0x1 then -- Not text frame (which is all we support)
249 log("warn", "Received frame with unsupported opcode %i", opcode); 263 log("warn", "Received frame with unsupported opcode %i", opcode);
250 return ""; 264 return "";
251 end 265 end
252 266
253 if frame.FIN then 267 return frame.data;
254 local data = t_concat(dataBuffer, "");
255 dataBuffer = nil;
256 return data;
257 end
258 return "";
259 end 268 end
260 269
261 conn:setlistener(c2s_listener); 270 conn:setlistener(c2s_listener);
262 c2s_listener.onconnect(conn); 271 c2s_listener.onconnect(conn);
263 272
280 session:close({ condition = "resource-constraint", text = "frame buffer exceeded" }); 289 session:close({ condition = "resource-constraint", text = "frame buffer exceeded" });
281 return; 290 return;
282 end 291 end
283 292
284 local cache = {}; 293 local cache = {};
285 local frame, length = parse_frame(frameBuffer); 294 local frame, length, partial = parse_frame(frameBuffer);
286 295
287 while frame do 296 while frame do
288 if length > stanza_size_limit then 297 frameBuffer:discard(length);
289 session:close({ condition = "policy-violation", text = "stanza too large" }); 298 local result, err_status, err_text = handle_frame(frame);
290 return; 299 if not result then
300 websocket_handle_error(session, err_status, err_text);
301 break;
291 end 302 end
292 frameBuffer:discard(length);
293 local result = handle_frame(frame);
294 if not result then break; end
295 cache[#cache+1] = filter_open_close(result); 303 cache[#cache+1] = filter_open_close(result);
296 frame, length = parse_frame(frameBuffer); 304 frame, length, partial = parse_frame(frameBuffer);
297 end 305 end
306
307 if partial then
308 -- The header of the next frame is already in the buffer, run
309 -- some early validation here
310 local frame_ok, err_status, err_text = validate_frame(partial, stanza_size_limit);
311 if not frame_ok then
312 websocket_handle_error(session, err_status, err_text);
313 end
314 end
315
298 return t_concat(cache, ""); 316 return t_concat(cache, "");
299 end); 317 end);
300 318
301 add_filter(session, "stanzas/out", function(stanza) 319 add_filter(session, "stanzas/out", function(stanza)
302 stanza = st.clone(stanza); 320 stanza = st.clone(stanza);