Heartbleed was a C bug, not a web bug
CVE-2014-0160 was an out-of-bounds read in OpenSSL C, not a JavaScript flaw. The real mechanism, the network-only telemetry gap, and what survived the patch.
CVE-2014-0160. OpenSSL 1.0.1 through 1.0.1f. A missing bounds check in the TLS heartbeat extension. That is Heartbleed. It was written in C. It had nothing to do with JavaScript.
The framing that ties Heartbleed to JavaScript’s rise in the browser is wrong, and the wrongness is the lesson. Conflating the two erases the root cause and replaces it with a mascot. Heartbleed was not a browser bug. It was not a JavaScript bug. It was not a memory-management failure in V8. It was an out-of-bounds read, CWE-125, in a C library that terminated TLS for a large fraction of the public web in April 2014. Netcraft put roughly half a million trusted HTTPS servers in scope at disclosure, about 17 percent of certificates seen as secure. CVSS v2 scored it 5.0 at the time. Retrofitted to CVSS v3.1 it sits at 7.5, vector AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N. High confidentiality impact, no integrity or availability impact, pre-authentication, network reachable.
The heartbeat extension is RFC 6520. It keeps a TLS or DTLS session alive without renegotiation. A client sends a heartbeat request carrying a type byte, a 16-bit payload length, the payload, and padding. The server echoes the payload back. The vulnerable code in tls1_process_heartbeat read the attacker-supplied length field, allocated a response buffer of that size, and called memcpy to copy that many bytes from the request record. It never checked that the request actually held that many bytes of payload. The length field claimed 65535. The real payload was one byte. memcpy copied 65535 bytes starting at the payload pointer: one byte of attacker data, then 65534 bytes of whatever sat adjacent in the heap. The server returned all of it.
That is the entire bug. No write. No corruption. A read primitive the attacker triggers at will, before authentication, over a standard TLS handshake, leaving the process running and the connection clean.
The bug exists because the length field was trusted as a description of the buffer instead of validated against it. The record carries its own length in s->s3->rrec.length. The payload length the heartbeat claims has to fit inside that. The comparison that enforces it, 1 + 2 + payload + 16 against the record length, was absent in the vulnerable builds and added in 1.0.1g. The fix is three lines. The defect was introduced on 31 December 2011 and shipped in OpenSSL 1.0.1 on 14 March 2012. The exposure window was the two years the library ran in production without that comparison, closed on 7 April 2014. The same author wrote the RFC implementation and the patch.
What the read returns is the heap of the process terminating TLS. That heap holds the server RSA private key, session keys, decrypted request bodies in flight, session cookies, and credentials other clients submitted seconds earlier. 64KB per request, no rate limit, no audit record. Repeat the request and walk the heap. This maps to MITRE T1190, exploitation of a public-facing application, with the objective being key and credential disclosure rather than code execution. The distinction matters for severity. A write primitive gives an attacker control of execution. A read primitive gives an attacker everything the process knows. For a TLS terminator, what the process knows is the key that decrypts every session it ever served.
The reason the read returned useful material reliably, rather than noise, is allocator behaviour. OpenSSL recycles buffers through its own freelist, OPENSSL_malloc sitting over the platform allocator, and the heartbeat response buffer is drawn from the same arena that recently held parsed TLS records, decrypted plaintext, and key structures. The 64KB window often overlapped freed allocations that had not been overwritten. Repeated requests sampled different positions as the heap shifted under live traffic. An attacker did not control the layout. The attacker did not need to. Volume plus a busy server turned an uncontrolled read into a near-complete dump of the process secrets over time, which is why key extraction held up under the public challenge instead of staying a corner case.
JavaScript enters this story only through a category error. JavaScript is garbage-collected and memory-safe at the language level. A script cannot index past an array into adjacent memory; the bound is in the specification and enforced by the engine. The memory-corruption bugs in browsers do not live in JavaScript. They live in the C++ engines that execute it, V8, SpiderMonkey, JavaScriptCore, and in the renderer’s C++ DOM and graphics code. Type confusion in TurboFan. Use-after-free in Blink. Integer overflow in a graphics path feeding a heap allocation. Those are the browser’s memory-safety failures, and they produce arbitrary read and write and code execution, which Heartbleed never did. Blaming JavaScript for Heartbleed points the analysis at the one layer of the browser stack that was not the problem.
The two share exactly one property, and it is the only one that carries. Both are written in memory-unsafe languages where a length or a type is trusted instead of checked. Heartbleed trusted a length field against a buffer. A V8 type confusion trusts a shape assumption the compiler did not guard. The bug class is one family: absence of an enforced bound on attacker-influenced data in C or C++. The language at the top of the stack is irrelevant. The language at the bottom is the whole problem. The script running in the page did not cause the corruption. The C++ underneath it did, the same way the C underneath the TLS terminator did.
Heartbleed was exploited in the wild. Cloudflare ran a public challenge after disclosure and confirmed private-key extraction from the heap was practical, not theoretical. Internet-wide scanning started within hours; the bug was trivial to test for and trivial to weaponise as a read, because triggering it required no valid session and the response handed the memory straight back. Some telemetry placed probing before public disclosure, though attribution to a specific actor stayed unproven. The Canada Revenue Agency lost roughly 900 social insurance numbers to it, with an arrest following. The Community Health Systems intrusion, 4.5 million patient records, was tied by responders to a Heartbleed read against a Juniper SSL VPN, with the actor attributed to a Chinese group. The patch did not end either incident. Every private key exposed to a vulnerable server had to be treated as compromised, every certificate reissued and revoked, every session secret rotated. Most operators patched the binary and stopped there. The residual exposure was the key material that had already left the building and could not be recalled.
The economics are worth stating plainly because they explain the speed of in-the-wild use. A write primitive demands an exploit chain: a leak, an ASLR defeat, a controlled corruption, a pivot to execution, often a second bug for the sandbox. A read primitive of this shape demands a single malformed packet and a loop. No memory grooming, no ROP chain, no payload staging that a sensor might catch. The cost to weaponise was effectively zero, the failure mode was a clean response rather than a crash that draws attention, and the yield was private keys. That ratio is what produced internet-scale scanning within hours and is the same ratio that makes network-facing over-reads in C the bug class worth hunting now.
In telemetry, Heartbleed is a network-only event, and this is where defenders read it wrong. The exploit produces nothing on the host. No process crash, because the read stays inside mapped memory and returns cleanly rather than walking into an unmapped page. No file written. No child process. No privilege change. No token manipulation. Sysmon records nothing. EDR records nothing. The Windows Security log records nothing. The TLS-terminating process logs an ordinary session. The only signal is on the wire: a TLS heartbeat record whose declared payload length exceeds the request that carried it, and a heartbeat response far larger than the request that triggered it, on content type 24. Snort and Suricata shipped signatures inside a day, Sourcefire publishing SIDs from 30510 onward matching the malformed heartbeat request and the oversized response. An IDS inspecting or terminating TLS at the perimeter could see it. An endpoint sensor could not. Any detection program that leaned on host telemetry was blind to this by construction, because the vulnerability never touched the host’s execution path. It read memory and answered a network request, and answering a network request is what the service is for.
That blindness is the durable lesson, not the logo and the disclosure website. The detection gap was structural, not a tuning miss. Pre-authentication, read-only, network-resident bugs do not generate host events, so the sensors most teams trust most see nothing. The same gap applies now to any over-read in a network-facing C or C++ service: TLS terminators, VPN concentrators, mail gateways, load balancers, anything parsing attacker-controlled length fields off the wire. The class did not retire with OpenSSL 1.0.1g. It reappears every time a length is trusted against a buffer in a language that will not stop the copy.
The patch boundary is clean. OpenSSL 1.0.1g and later validate the heartbeat payload length against the record length and drop the malformed request. 1.0.1 through 1.0.1f and 1.0.2-beta1 are vulnerable. RFC 6520 is not the flaw; the implementation that failed to bound-check it was. What persisted past the patch was everything the read had already returned and the architectural condition underneath it, a length field driving a memcpy in a language with no bounds enforcement. JavaScript did not cause Heartbleed. Memory-unsafe C did. The version that blames the browser is comfortable because it points at a surface everyone can see and run in a tab. The accurate version points at the C standard library sitting under the entire stack, and that is the harder thing to retire, which is why the bug class is still shipping.
#ad Contains an affiliate link.
Keep Reading
duckdbDuckDB trusts persisted blocks attackers control
DuckDB runs in-process as a C++ library. Its immutability and checksum assumptions create a quiet memory-corruption surface that host EDR never sees.
nginxNGINX rewrite module bleeds memory
CVE-2026-42945 places a heap buffer overflow inside NGINX's rewrite module, on the request path. Defect class confirmed. Impact not confirmed.
identity verification2023 mistakes an IP address for a passport
Forcing real ID on all internet traffic relocates an unsolved identity problem to a layer that cannot verify the subject and creates a higher value target.
Stay in the loop
New writing delivered when it's ready. No schedule, no spam.