Exploit
Captured source
source ↗Reverse engineering Claude's CVE-2026-2796 exploit \ Anthropic Frontier Red Team Reverse engineering Claude's CVE-2026-2796 exploit Mar 6, 2026
Evyatar Ben Asher, Keane Lucas, Nicholas Carlini, Newton Cheng, and Daniel Freeman Introduction Today we published an update on our collaboration with Mozilla, in which Claude Opus 4.6 found 22 vulnerabilities in Firefox over the course of two weeks. As part of that work, we evaluated whether Claude could go further: exploit the bugs, as well as find them. This blog post will deep dive into how Claude wrote an exploit for CVE-2026-2796 (now patched). This is another data point for the trajectory of LLM’s cyber capabilities. In September, we noted that Claude's success rate on Cybench had doubled in six months. In early February we demonstrated that Claude’s success rate on Cybergym doubled in four months. We’re sharing this case study to provide an early glimpse into what we expect will be LLMs’ improving ability to author exploits. To be clear, the exploit that Claude wrote only works within a testing environment that intentionally removes some of the security features of modern web browsers. Claude isn't yet writing “full-chain” exploits that combine multiple vulnerabilities to escape the browser sandbox, which are what would cause real harm. And recall that Opus 4.6 only turned a vulnerability into an exploit in two cases (given hundreds of chances at dozens of bugs). But the success we did observe signals that Claude is getting much closer to being capable of full-chain exploits, and we think this result is an important early warning sign of where capabilities are heading. When we say “Claude exploited this bug,” we really do mean that we just gave Claude a virtual machine and a task verifier, and asked it to create an exploit. To be thorough we also gave it about 350 chances to succeed. We then reverse-engineered the proof-of-concept exploit that Claude produced, both to verify the result and to update our understanding of the model's emergent capabilities. This blog is structured around what we learned during that process. We’ll cover just enough JavaScript to understand the vulnerability, explore the vulnerability details at a conceptual level, and then dig into Claude's transcripts to see how it built the exploit primitives. Javascript primer CVE-2026-2796 is officially a JIT miscompilation in the JavaScript WebAssembly component. JIT and WebAssembly have been well-documented elsewhere, and we'd recommend those resources for a deeper background. You don’t need to understand much about JIT to follow this blog, but we’ll cover the subset of WebAssembly (Wasm) that is relevant. At a high level, Wasm is a way to run compiled code inside the browser. The fundamental unit of code in Wasm is called a module. A Wasm module is a self-contained unit of code; think of it like a .so or .dll . A module can export functions for the outside world to call and import functions that the host (JavaScript) provides at instantiation time.The import/export boundary is where our bug lives. When JavaScript instantiates a module, it passes in an import object: a bag of functions the module expects to find. If you pass a Wasm function whose type signature doesn't match what the module declared, the engine rejects it outright with a LinkError . JS functions get a pass here because they're dynamically typed, but the engine has a different safety mechanism for these: every call to a JS-backed import goes through an interop layer that converts Wasm values to JS values and back again. This conversion means data passing through the JS/Wasm boundary is never reinterpreted as raw bits, making type mismatches harmless. Together, these two mechanisms (instantiation-time type checks for Wasm functions and runtime conversion checks for JS functions) form the engine's type safety boundary. Our bug sneaks between both. Let’s dive into a quick example. Below is a WebAssembly Text (WAT) format module, called example . It imports a function called log, from the env namespace that takes in a 32-bit integer as its first (and only) parameter. It exports a function called go, which puts a 32-bit integer constant value (in this case, the value 42) on the operand stack and calls the 0th defined function in the module, which happens to be log . The JavaScript code instantiates that module by passing in its own implementation of log, and calls the go function exported by that module. If you were to run this code, you would see console output that says, “wasm says: 42”. If you want to try it yourself, Appendix A.1 has a self-contained version you can paste into any browser console. //(example // (import "env" "log" (func $log (param i32))) ;; import a JS function // (func (export "go") // i32.const 42 // call $log)) ;; call env.log(42)
const instance = new WebAssembly.Instance(example, { env: { log: (x) => log("wasm says:", x) } }); instance.exports.go(); // "wasm says: 42"
The vulnerability Claude identified shows up when the function you pass in isn’t a plain function but a Function.prototype.call.bind(...) wrapper. In JavaScript, every function has a .bind() method that creates a new function with a fixed this value. In JavaScript, this is the pointer to the current class object. Function.prototype.call.bind(someFunc) takes the built-in call method (which lets you invoke any function with an explicit this ) and locks its this to someFunc . The result is an argument-shifting wrapper: function greet(msg) { return msg + " " + this.name; }
const bound = Function.prototype.call.bind(greet); bound({name: "Alice"}, "Hello"); // "Hello Alice" // ^ becomes this ^ becomes msg
Firefox has a fast path for this case (that is, a special codepath in the interpreter that makes this function run more efficiently), and that fast path is where our vulnerability lives. Vulnerability primer Now that we understand how Wasm modules and bind works, let’s review the discovered vulnerability’s root cause. To exercise the bug, you need two modules: one that imports a function and calls it, and another that exports a function. Consider the two modules below: ;; Module A: imports a function and calls it (module (import "env" "imp" (func (param i32) (result i32))) (func (export "go") (param i32) (result i32) local.get 0 call 0)) ;; go(x) = imp(x) ;; Module B: exports a simple identity function (module (func (export "f") (param i32)...
Excerpt shown — open the source for the full document.
Notability
notability 7.0/10Notable research post from a leading AI lab.