安全研究:利用 GPT 5.4 黑进 Safari 浏览器

📡 Imperva Blog · 2026-04-23

Hacking Safari with GPT 5.4

CVE-2026-20664

When Anthropic unveiled Mythos and Project Glasswing, the reaction was immediate and polarized. Some dismissed it as fear-driven marketing, while others treated it as a credible shift in the threat landscape.

Like with many things, the truth is probably somewhere in the middle. I wanted to test that for myself, and since I recently got access to OpenAI’s Trusted Access for Cyber program, I decided to take it for a spin.

GPT-5.4 identified the bugs and helped assemble a working exploit chain, but it wasn’t a simple “build me an exploit” prompt. Guiding it required domain knowledge, iterative probing, and knowing which paths were actually exploitable.

On modern browsers like Safari, exploitation is less about finding bugs and more about finding bugs that still matter after multiple layers of defense.

The bug I’m going to talk about today sits in a more interesting category. The bug itself looked contained, and in many ways it was. It did not provide a path to RCE or a sandbox escape. What it did instead was cross a different boundary entirely: it broke the Same-Origin Policy.

If you visited a malicious page from any Apple device, it could read authenticated cross-origin data from other sites you use, including access tokens and other sensitive data, making account takeover trivial.

The video below shows the PoC we sent Apple, demonstrating leakage of sensitive data from both Apple Connect and iCloud / Apple ID endpoints. Although this demo focuses on Apple services, the issue affects all websites. This means that by visiting a malicious website, sensitive data from other domains is at risk of being leaked.

The Sandbox Russian Doll

Browser exploitation in 2026 is a lot like being trapped in a Russian doll.

You start in the smallest doll, and every time you escape one layer you discover you are still trapped inside another one.

Finding a low-level memory bug is not the same thing as finding an exploit. Most of these bugs die in the gap between “memory corruption happened” and “something meaningful crossed a security boundary.”

On the outside you have the browser process model. Even if renderer code goes wrong, the browser is trying very hard to keep that damage inside the web content process.

Inside that you have the web security model: Same-Origin Policy, CORS, opaque responses, cookie scoping, and credential modes. Even if a page can trigger a cross-origin request, the renderer, and especially the Gigacage, should not be able to access the response bytes. Right?…

The Bug

The original bug lives in the refresh logic for non-shared resizable WebAssembly memory.

When a non-shared WebAssembly.Memory grows in BoundsChecking mode, JavaScriptCore can replace the underlying memory handle. That part is not the bug. The bug is what happens after that to the JS-visible resizable buffer returned by memory.toResizableBuffer().

The bug is simple enough that once I saw it, it was hard to unsee it. Safari’s grow path effectively does this:

And the refresh step effectively does this:

After memory.grow(), WebKit updates the buffer metadata, but leaves m_data pointing at the old freed allocation.

So after a grow, JavaScript can hold a buffer whose reported size is new, whose handle is new, but whose actual data pointer still references the old freed Primitive Gigacage allocation.

That turns into a stale typed-array window over freed memory.

On its own, this is already a real bug. But we’re still stuck inside the JavaScriptCore gigacage, effectively sandboxed. Without a second bug to break out into the renderer, it doesn’t chain into anything meaningful. What we have is a solid first-stage primitive, but no real security impact on its own.

Why it did not look exploitable at first

The stale window is confined to the Primitive Gigacage, which immediately limits what you can do with it. Many typical targets either never land there, lack useful structure, or fail to produce any cross-boundary effect.

So early on, it had all the hallmarks of a bug that looks promising but rarely goes the distance:

  • easy source-level root cause
  • visible stale memory behavior
  • real reclaim
  • no clean escape path

This is where a lot of low-level browser bugs die.

What changed the problem was a very different framing: maybe I did not need to escape the cage at all.

Maybe I just needed the browser to place something valuable inside it.

The Pivot

Instead of asking “how do I get from my stale WASM view to some protected browser state?” I started asking a better question:

“What browser code takes data that JavaScript is not allowed to read, but still copies that data into normal renderer memory?”

Because that is all I need.

I don’t need to break the abstraction.

I just need the browser to break it for me.

That naturally narrows the search space to subsystems that:

  • handle sensitive cross-origin data, and
  • still allocate ArrayBuffer-backed memory as part of their internal pipeline

That points straight at Fetch. The Fetch API clearly indicates that the response is opaque, meaning that its headers and body are not available to JavaScript.

Opaque Responses Are Supposed to Be Opaque

At the API level, the Fetch model here is straightforward.

If I do a cross-origin request with:

fetch(url, { mode: “no-cors”, credentials: “include” });

The browser may send the request, including cookies depending on context, but JavaScript receives an opaque response.

That means:

  • I can hold the Response object
  • but I cannot read the body bytes

And WebKit enforces that in the obvious place:

FetchBodyOwner::readableStream() blocks opaque bodies via isBodyNullOrOpaque().

So at first glance, everything looks fine. The body is hidden. The policy is enforced. Same-Origin Policy survives another day.

Except it does not.

The Fetch Behavior that Broke the Modal

The surprising part is Response.clone().

If FetchResponse::clone() is called while the response is still loading, WebKit will internally create a readable stream so it can tee the body between the original response and the clone.

That internal path does not apply the same opaque-body check first.

And once that happens, hidden response bytes start becoming very real renderer objects.

This is the part that made me stop and stare at the source, because the mismatch is right there.

The normal body path blocks opaque responses:

But FetchResponse::clone() does this while the response is still loading:

That is why it works.

The visible accessor path says “opaque bodies do not get a stream.” The clone path says “if it is still loading, create a stream so both clones can tee it.”

That second path is exactly what I needed.

The data flows through normal ArrayBuffer creation paths:

  • buffered chunks go through tryCreateArrayBuffer()
  • later chunks go through takeAsArrayBuffer()
  • shared buffer data gets copied into ordinary ArrayBuffer allocations inside the renderer

So the policy ends up split in two:

  • the public Fetch API says the body is opaque
  • the renderer still materializes the opaque body into readable byte arrays during clone-time streaming

Combined with the stale WASM window, it becomes a SOP break.

The Chain

At a high level, the exploit became:

  • Force the target WASM memory into the BoundsChecking path.
  • Call memory.toResizableBuffer().
  • Grow the memory.
  • Keep the stale resizable buffer whose pointer still targets freed Primitive Gigacage pages.
  • Trigger a cross-origin fetch(…, { mode: “no-cors”, credentials: “include” }).
  • Call response.clone() while the response is still loading.
  • Let Fetch internals materialize the hidden body bytes into ordinary renderer ArrayBuffers.
  • Reclaim the stale WASM-covered pages with those allocations.
  • Read the cross-origin bytes through the stale view.

That is the entire trick.

I never needed response.text(). I never needed response.arrayBuffer(). I never needed the public API to hand me the body.

The browser copied the body into m


📌 来源: Imperva Blog | 🆔 CVE-2026-20664 | 📅 2026-04-23

[!] CONTACT_CHANNELS

如需商务合作、技术咨询或漏洞反馈,请通过以下离岸节点联系作者。

> PING_AUTHOR (@A1RedTeam)