Stop a heartbeat over-read

A model of C memory: a TLS heartbeat handler echoes as many bytes as the request claims, not as many as arrived, so a short request returns the next client’s cookie, as Heartbleed did. Add the bounds checks.

Requires Node.js 22 or newer. No package installation or server needed. To work in a container instead, open the folder with VS Code Dev Containers or GitHub Codespaces: .devcontainer/devcontainer.json uses the Dev Containers Node.js 24 image, pinned by digest.
1. Unzip all files into one folder.
2. In that folder, run: node --test exercise.test.mjs
3. Edit heartbeat.mjs, then run the same command again.

The starter deliberately fails some tests. A successful repair passes every case, including cases that catch common wrong fixes. The browser runs this same cases.mjs with the same fixtures; locally Node runs your code directly. If you completed the optional first stage in the browser, Download my work adds your input as stage1-input.json and the tests include it. Only run code you wrote or reviewed.

1. Watch the over-read
Run the starter. In the over-read case a one-byte payload claims 160 bytes, and the response carries the neighbouring buffer: another client’s HTTP request with its session cookie. Near the end of memory the read runs off the heap, which in C is a crash.

2. Bound the parser by the record, not the heap
The record is record.length bytes. Before reading anything, make sure the header and the minimum padding fit (1 + 2 + 16 bytes). A check against memory.length is the classic wrong fix: the read stays inside the heap and still leaks the neighbour.

3. Check the claimed length against what arrived
Read payload_length big-endian and discard the message unless 1 + 2 + payload_length + 16 <= record.length. That one comparison is the fix OpenSSL shipped in 1.0.1g. Discard any type other than 1.

4. Keep real heartbeats working
Ordinary requests, an empty payload, a long payload, extra padding and padding of exactly 16 bytes must still be echoed exactly: the payload only, never the padding.

SUPPLIED CONTRACT
JavaScript checks every array index, so the lab models C memory: memory is an array of byte values standing for the process heap, with several buffers side by side, and reading past one buffer returns its neighbour’s bytes, as memcpy does in C. heartbeatResponse(memory, record) handles one received heartbeat message, which occupies record.length bytes of memory starting at record.offset. The message (RFC 6520 §4) is: type (1 byte; 1 is heartbeat_request), payload_length (2 bytes, big-endian, network byte order), the payload, then at least 16 bytes of padding, which the receiver ignores. Return the payload as an array of byte values to echo it, or null to discard the message silently. RFC 6520 requires discarding a message whose payload_length is too large, which with the 16 padding bytes means 1 + 2 + payload_length + 16 must not exceed record.length; answer only heartbeat_request messages. Inside your code you may wrap memory with Uint8Array.from(memory) and read it with a DataView, whose getUint16 is big-endian by default.

REFERENCE
reference.mjs contains an explained solution: A request with payload "ping-0001", an empty payload, a 120-byte payload, 40 bytes of padding and exactly 16 bytes of padding are echoed exactly. Discarded with null: a one-byte payload claiming 160 bytes (the over-read, even though 160 bytes fit in memory), a payload_length that leaves no room for padding, one that leaves 15 bytes of padding, a record of 2 bytes, a heartbeat_response (type 2) and an unknown type, and a record at the end of memory claiming 65,535 bytes. Reading payload_length little-endian fails the ordinary request, because 0x0009 becomes 0x0900.

SCOPE
This is a model: a JavaScript array cannot be over-read, so the lab places buffers side by side in one array to show what an unchecked memcpy does in C. Real defences are memory-safe languages for new code, and in C and C++ bounds checks derived from the format, sanitizers (AddressSanitizer reports a heap-buffer-overflow read here) and fuzzing; the native memory-safety project on the labs page compiles a real C parser with them. OpenSSL fixed the bug in 1.0.1g; its advisory of 7 April 2014 says the missing bounds check could reveal up to 64k of memory to a connected client or server.

NEXT TASK
Find a parser in your code base, in any language, that reads a length field from its input. Where does it compare that length with the number of bytes it actually received?

SOURCE
https://www.rfc-editor.org/rfc/rfc6520.html#section-4
