Skip to content

Commit

Permalink
Use plausible example for WebSocket unmasking (#38145)
Browse files Browse the repository at this point in the history
  • Loading branch information
Josh-Cena authored Feb 16, 2025
1 parent 2fdcb82 commit fec5ea2
Showing 1 changed file with 95 additions and 41 deletions.
136 changes: 95 additions & 41 deletions files/en-us/web/api/websockets_api/writing_websocket_servers/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand Down

0 comments on commit fec5ea2

Please sign in to comment.