From fec5ea2b4f7249f95ad3fb1fd1a555db3fe47c35 Mon Sep 17 00:00:00 2001 From: Joshua Chen Date: Sun, 16 Feb 2025 05:59:33 -0500 Subject: [PATCH] Use plausible example for WebSocket unmasking (#38145) --- .../writing_websocket_servers/index.md | 136 ++++++++++++------ 1 file changed, 95 insertions(+), 41 deletions(-) diff --git a/files/en-us/web/api/websockets_api/writing_websocket_servers/index.md b/files/en-us/web/api/websockets_api/writing_websocket_servers/index.md index 3b68b5c3b2f4e50..cb351011d1a1f1e 100644 --- a/files/en-us/web/api/websockets_api/writing_websocket_servers/index.md +++ b/files/en-us/web/api/websockets_api/writing_websocket_servers/index.md @@ -88,46 +88,72 @@ Either the client or the server can choose to send a message at any time — tha Each data frame (from the client to the server or vice versa) follows this same format: -```bash -Frame format: - - 0 1 2 3 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-------+-+-------------+-------------------------------+ - |F|R|R|R| opcode|M| Payload len | Extended payload length | - |I|S|S|S| (4) |A| (7) | (16/64) | - |N|V|V|V| |S| | (if payload len==126/127) | - | |1|2|3| |K| | | - +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + - | Extended payload length continued, if payload len == 127 | - + - - - - - - - - - - - - - - - +-------------------------------+ - | |Masking-key, if MASK set to 1 | - +-------------------------------+-------------------------------+ - | Masking-key (continued) | Payload Data | - +-------------------------------- - - - - - - - - - - - - - - - + - : Payload Data continued ... : - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - | Payload Data continued ... | - +---------------------------------------------------------------+ +```plain +Data frame from the client to server (message length 0–125): + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 ++-+-+-+-+-------+-+-------------+-------------------------------+ +|F|R|R|R| opcode|M| Payload len | Masking-key | +|I|S|S|S| (4) |A| (7) | (32) | +|N|V|V|V| |S| | | +| |1|2|3| |K| | | ++-+-+-+-+-------+-+-------------+-------------------------------+ +| Masking-key (continued) | Payload Data | ++-------------------------------- - - - - - - - - - - - - - - - + +: Payload Data continued ... : ++ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +| Payload Data continued ... | ++---------------------------------------------------------------+ + +Data frame from the client to server (16-bit message length): + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 ++-+-+-+-+-------+-+-------------+-------------------------------+ +|F|R|R|R| opcode|M| Payload len | Extended payload length | +|I|S|S|S| (4) |A| (7) | (16) | +|N|V|V|V| |S| (== 126) | | +| |1|2|3| |K| | | ++-+-+-+-+-------+-+-------------+-------------------------------+ +| Masking-key | ++---------------------------------------------------------------+ +: Payload Data : ++ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +| Payload Data continued ... | ++---------------------------------------------------------------+ + +Data frame from the server to client (64-bit payload length): + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 ++-+-+-+-+-------+-+-------------+-------------------------------+ +|F|R|R|R| opcode|M| Payload len | Extended payload length | +|I|S|S|S| (4) |A| (7) | (64) | +|N|V|V|V| |S| (== 127) | | +| |1|2|3| |K| | | ++-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + +| Extended payload length continued | ++ - - - - - - - - - - - - - - - +-------------------------------+ +| | Masking-key | ++-------------------------------+-------------------------------+ +| Masking-key (continued) | Payload Data | ++-------------------------------- - - - - - - - - - - - - - - - + +: Payload Data continued ... : ++ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +| Payload Data continued ... | ++---------------------------------------------------------------+ ``` This means that a frame contains the following bytes: - First byte: - - bit 0: FIN - - bit 1: RSV1 - - bit 2: RSV2 - - bit 3: RSV3 - - bits 4-7 OPCODE -- Bytes 2-10: payload length (see [Decoding Payload Length](#decoding_payload_length)) -- If masking is used, the next 4 bytes contain the masking key (see [Reading and unmasking the data](#reading_and_unmasking_the_data)) -- All subsequent bytes are payload - -The MASK bit tells whether the message is encoded. Messages from the client must be masked, so your server must expect this to be 1. (In fact, [section 5.1 of the spec](https://datatracker.ietf.org/doc/html/rfc6455#section-5.1) says that your server must disconnect from a client if that client sends an unmasked message.) When sending a frame back to the client, do not mask it and do not set the mask bit. We'll explain masking later. _Note: You must mask messages even when using a secure socket._ RSV1-3 can be ignored, they are for extensions. - -The opcode field defines how to interpret the payload data: `0x0` for continuation, `0x1` for text (which is always encoded in UTF-8), `0x2` for binary, and other so-called "control codes" that will be discussed later. In this version of WebSockets, `0x3` to `0x7` and `0xB` to `0xF` have no meaning. - -The FIN bit tells whether this is the last message in a series. If it's 0, then the server keeps listening for more parts of the message; otherwise, the server should consider the message delivered. More on this later. + - Bit 0 FIN: tells whether this is the last message in a series. If it's 0, then the server keeps listening for more parts of the message; otherwise, the server should consider the message delivered. More on this later. + - Bit 1–3 RSV1, RSV2, RSV3: can be ignored, they are for extensions. + - Bits 4-7 OPCODE: defines how to interpret the payload data: `0x0` for continuation, `0x1` for text (which is always encoded in UTF-8), `0x2` for binary, and other so-called "control codes" that will be discussed later. In this version of WebSockets, `0x3` to `0x7` and `0xB` to `0xF` have no meaning. +- Bit 8 MASK: tells whether the message is encoded. Messages from the client must be masked, so your server must expect this to be 1. (In fact, [section 5.1 of the spec](https://datatracker.ietf.org/doc/html/rfc6455#section-5.1) says that your server must disconnect from a client if that client sends an unmasked message.) Server-to-client message are not masked and have this bit set to 0. We'll explain masking later, in [reading and unmasking the data](#reading_and_unmasking_the_data). _Note: You must mask messages even when using a secure socket._ +- Bits 9–15: payload length. May also include the following 2 bytes or 8 bytes; see [Decoding Payload Length](#decoding_payload_length). +- If masking is used (always true for client-to-server messages), the next 4 bytes contain the masking key; see [Reading and unmasking the data](#reading_and_unmasking_the_data). +- All subsequent bytes are payload. ### Decoding Payload Length @@ -139,17 +165,45 @@ To read the payload data, you must know when to stop reading. That's why the pay ### Reading and unmasking the data -If the MASK bit was set (and it should be, for client-to-server messages), read the next 4 octets (32 bits); this is the masking key. Once the payload length and masking key is decoded, you can read that number of bytes from the socket. Let's call the data `ENCODED`, and the key `MASK`. To get `DECODED`, loop through the octets (bytes a.k.a. characters for text data) of `ENCODED` and XOR the octet with the (i modulo 4)th octet of `MASK`. In pseudocode (that happens to be valid JavaScript): +If the MASK bit was set (and it should be, for client-to-server messages), read the next 4 octets (32 bits); this is the masking key. Once the payload length and masking key is decoded, you can read that number of bytes from the socket. Let's call the data `ENCODED`, and the key `MASK`. To get `DECODED`, loop through the octets of `ENCODED` and XOR the octet with the (i modulo 4)th octet of `MASK`. Using JavaScript as an example: ```js -const MASK = [1, 2, 3, 4]; // 4-byte mask -const ENCODED = [105, 103, 111, 104, 110]; // encoded string "hello" +// The function receives the frame as a Uint8Array. +// firstIndexAfterPayloadLength is the index of the first byte +// after the payload length, so it can be 2, 4, or 10. +function getPayloadDecoded(frame, firstIndexAfterPayloadLength) { + const mask = frame.slice( + firstIndexAfterPayloadLength, + firstIndexAfterPayloadLength + 4, + ); + const encodedPayload = frame.slice(firstIndexAfterPayloadLength + 4); + // XOR each 4-byte sequence in the payload with the bitmask + const decodedPayload = encodedPayload.map((byte, i) => byte ^ mask[i % 4]); + return decodedPayload; +} + +const frame = Uint8Array.from([ + // FIN=1, RSV1-3=0, opcode=0x1 (text) + 0b10000001, + // MASK=1, payload length=5 + 0b10000101, + // 4-byte mask + 1, 2, 3, 4, + // 5-byte payload + 105, 103, 111, 104, 110, +]); + +// Assume you got the number 2 from properly decoding the payload length +const decoded = getPayloadDecoded(frame, 2); +``` + +Now you can figure out what `decoded` means depending on your application. For example, you can [decode](/en-US/docs/Web/API/TextDecoder) it as UTF-8 if it's a text message. -// Create the byte Array of decoded payload -const DECODED = Uint8Array.from(ENCODED, (elt, i) => elt ^ MASK[i % 4]); // Perform an XOR on the mask +```js +console.log(new TextDecoder().decode(decoded)); // "hello" ``` -Now you can figure out what **DECODED** means depending on your application. +Masking is a security measure to avoid malicious parties from predicting the data that is sent to the server. The client will generate a cryptographically random masking key for each message. ### Message Fragmentation