Wanneer Agentic Glue Smelt: Het Exploiteren van Cloudflare Code Mode en Workers - Check Point Research

Kernpunten

  • Check Point Research heeft Cloudflare Code Mode geanalyseerd, een techniek die verandert hoe AI-agents gebruikmaken van MCP door tools om te zetten in een TypeScript API waar het model code tegen kan schrijven.
  • Het onderzoek bracht vijf kwetsbaarheden aan het licht in workerd, de open-source runtime achter Code Mode en Cloudflare Workers. Twee hiervan werden door Cloudflare als 'Kritiek' geclassificeerd.
  • De impact is groot: volgens cijfers van Cloudflare zelf wordt Workers gebruikt door miljoenen ontwikkelaars, verwerkt het miljoenen verzoeken per seconde en verzorgt het meer dan 10% van al het verkeer op het netwerk van Cloudflare.
  • Omdat workerd zowel de sandboxes van Code Mode als de tenant-isolatie van Workers ondersteunt, creëren deze bevindingen risico's op sandbox-ontsnappingen en cross-tenant blootstelling.
  • De beheerde Workers-omgeving van Cloudflare is in productie hersteld. Zelfgehoste workerd / Code Mode implementaties moeten worden bijgewerkt naar v1.20260619.1.
  • Check Point Research heeft proof-of-concept code vrijgegeven als onderdeel van de presentatie op Black Hat USA 2026.

De korte versie

We waren van plan om Cloudflare Code Mode te breken, maar eindigden met het breken van Cloudflare Workers. We deden beide door ons te richten op workerd, de runtime onder beide systemen: een in-process sandbox die volledig vertrouwt op V8 om onbetrouwbare code te isoleren.

We vonden vijf memory-corruption bugs in de native C++ van workerd (de "glue" tussen JavaScript en de runtime), en zetten deze om in twee end-to-end aanvallen:

  1. Cross-tenant heap swipe: Een out-of-bounds read in URLPattern stelt één Worker in staat om over de gedeelde process heap te reiken en geheimen van een andere tenant te stelen.
  2. Code Mode sandbox escape: Startend vanuit een prompt injection, zorgt een use-after-free in node:zlib voor een uitbraak uit de sandbox, waardoor native code op de host kan worden uitgevoerd.

---

Deel I – Het doel begrijpen

1. Waar dit begon: Code Mode

Code Mode is de interpretatie van Cloudflare over LLM-toolgebruik. In plaats van dat een model één voor één gestructureerde tool-aanroepen verzendt, stelt Code Mode de beschikbare tools beschikbaar als een getypeerde TypeScript API. Het model kan vervolgens zelf code schrijven die deze tools aanroept, inclusief loops, conditionele logica en datamanipulatie.

In de traditionele MCP/tool-calling loop stuurt het model één {tool, args} aanroep, de agent voert deze uit en voert het resultaat terug naar het model. Elke stap is een nieuwe model-aanroep en meestal een netwerk round-trip. Code Mode reduceert dit: het model schrijft één programma dat vele tool-aanroepen zelf orkestreert (lokaal loopen, vertakken en combineren van tussenresultaten), waardoor alleen de uiteindelijke output terugkeert naar het model.

Cloudflare stelt dat LLM's, die getraind zijn op enorme hoeveelheden echte code, simpelweg beter zijn in het schrijven van een programma tegen een getypeerde API dan in het verzenden van lange ketens synthetische tool-aanroepen.

Deze code moet ergens worden uitgevoerd, en die plek is workerd, de runtime achter Cloudflare Workers.

2. De oorsprong van workerd

Om workerd te begrijpen, moeten we kijken naar het product waarvoor het is gebouwd: Cloudflare Workers. Workers is het serverless platform van Cloudflare; je uploadt code en Cloudflare voert deze uit aan de 'edge', in datacenters dicht bij de gebruiker, on-demand voor elk verzoek.

Dit model creëert een complex isolatieprobleem. Cloudflare voert code uit van een enorm aantal verschillende klanten. Om latentie en kosten laag te houden, worden veel van hen op dezelfde machines geplaatst, en zoals we zullen zien, in hetzelfde proces. De klassieke oplossing (een container of VM per tenant) is te zwaar; deze veroorzaakt koude starts van tientallen tot honderden milliseconden en een aanzienlijk geheugenverbruik.

De oplossing van Cloudflare is isolatie op het niveau van de language-runtime in plaats van het OS-niveau, gebruikmakend van V8 isolates (hetzelfde mechanisme dat Chrome gebruikt om browsertabs te scheiden). Een isolate is een lichtgewicht, onafhankelijke JavaScript-context. Meerdere isolates kunnen binnen één proces leven, starten in enkele milliseconden en vormen de beveiligingsgrens tussen tenants.

De trade-off is dat deze grens een softwarematige grens is binnen één gedeelde adresruimte, niet een hardware- of kernelgrens. Onbetrouwbare code draait in-process, en het hele model rust op de integriteit van de isolate.

workerd is de runtime die dit implementeert. Het was jarenlang closed-source (Workers lanceerde in 2017), maar Cloudflare bracht workerd in september 2022 als open source uit. Dit is precies de omgeving waarin Code Mode de door het model gegenereerde code uitvoert.

3. Waarom workerd de logische sandbox was voor Code Mode

Code Mode moet onbetrouwbare, door een model geschreven code kunnen uitvoeren en moet ervoor zorgen dat deze code alleen toegang heeft tot de gedeclareerde MCP-tools en niets anders. workerd lost beide op:

Het uitvoeren van onbetrouwbare tenant-code in-process is het primaire doel van workerd. Hierdoor kan Code Mode de rest afschermen: geen bestandssysteem, geen willekeurig netwerk (fetch() en connect() gooien simpelweg fouten) en tools die alleen via bindings toegankelijk zijn. Cloudflare heeft geen nieuwe sandbox gebouwd voor Code Mode; ze hebben de sandbox hergebruikt die ze al vertrouwen om miljoenen Workers te isoleren.

4. Waarom we ons op workerd richtten

Wanneer men Code Mode probeert te breken, is de meest logische plek de naad tussen Code Mode en workerd (de integratielaag). Het direct aanvallen van de runtime zelf is een ongebruikelijke stap.

Vijf redenen waarom we dit toch deden:

  1. Een in-process sandbox is een riskante weddenschap. Isolatie zonder OS-boundary betekent geen VM en geen container, alleen een V8 isolate. Dit plaatst het hele beveiligingsmodel op één softwarematige grens.
  2. workerd had nauwelijks publieke controle. In tegenstelling tot V8, dat continu wordt geanalyseerd, was er weinig publiek onderzoek naar kwetsbaarheden in workerd.
  3. Het aanvalsoppervlak is enorm. Naast V8 heeft workerd eigen implementaties van Web/Node API's, geschreven in C++ en bereikbaar vanuit onbetrouwbaar JavaScript.
  4. De impact reikt tot Cloudflare Workers. workerd is niet alleen de runtime voor Code Mode, maar ook de motor achter een van de meest gebruikte serverless platforms op internet.
  5. AI-beveiliging heeft ook een low-level kant. Naast high-level frameworks verdienen de interne lagen waar agents op vertrouwen ook onderzoek.

5. De kooi, memory protection keys en Node

Omdat V8 een geschiedenis heeft van geheugenfouten, hanteert Cloudflare gelaagde verdedigingen:

Verdedigingen:

  1. De V8 sandbox ("the cage"): Beperkt JS-bereikbare objecten zodat een gecorrumpeerd object geen pointers buiten de kooi kan smeden.
  2. Memory protection keys (MPK / pkeys): Hardwarematige tags op isolate-group geheugen, zodat zelfs bij arbitrary read/write binnen één V8-isolate, een aanvaller niet in de pagina's van een andere tenant kan lezen.
  3. De L2 proces sandbox: Een tweede laag (Linux namespaces plus seccomp) die alle toegang tot het bestandssysteem en directe netwerktoegang blokkeert.

Aanvalsoppervlak: Node.js-functionaliteit is essentieel voor JavaScript. workerd implementeert daarom een groot deel van de Node API in C++, ontsloten via de "JavaScript Glue" (JSG) laag. Node was nooit ontworpen voor een threat model waarbij de aanvaller de JavaScript schrijft, wat veel extra native code op de grens brengt die standaard is ingeschakeld (bijv. require('node:crypto')).

Dit betekent meer native objecten die worden gealloceerd op de tcmalloc heap, die niet wordt beveiligd door de "cage" of memory protection keys.

6. Conclusie

We hebben ons gericht op de JSG-code van workerd. Omdat native objecten hierop worden gealloceerd op de tcmalloc heap (buiten de kooi en MPK), is een bug hier niet ingekaderd zoals een V8-bug dat zou zijn.

We vonden vijf kwetsbaarheden in de eigen native code van workerd, waaruit we twee end-to-end exploits ontwikkelden:

  • Code Mode sandbox escape: Via prompt injection wordt het model gestuurd om TypeScript te schrijven met memory-corruption, wat leidt tot native code execution op de host.
  • Cross-tenant secret leak: Een kwaadaardige Worker in de gedeelde pool kan het geheugen van een andere tenant lezen en geheimen lekken uit het gedeelde proces.

Let op: We hebben deze exploits niet zelf uitgevoerd in de productieomgeving van Cloudflare, maar geverifieerd op de self-hosted versie van workerd om risico's voor andere tenants te vermijden.

---

Deel II – De kwetsbaarheden

7. URLPattern out-of-bounds read

URLPattern is een Web API voor het matchen van een URL tegen een patroon. workerd biedt dit aan Workers aan. We vonden een out-of-bounds (OOB) read in zowel de originele implementatie als de nieuwere Ada-gebaseerde implementatie.

7.1 De root cause

URLPattern zet een patroon om in een reguliere expressie. Bij het uitvoeren (.exec()) produceert dit twee lijsten: de gematchte waarden en de groepsnamen.

In de native implementatie wordt een loop gebruikt om deze te koppelen:

  • length komt van V8 (het aantal capture-groepen dat de regex daadwerkelijk produceerde).
  • nameList komt uit de eigen parsing van URLPattern.

Het probleem is dat de groepsberekening van URLPattern een groep mist wanneer deze genest is in een andere groep, terwijl V8 elke groep telt. Bij een patroon als (ab(cde)) ziet V8 twee groepen, maar URLPattern slechts één. Hierdoor loopt de loop één stap te ver over de nameList heen → een out-of-bounds read.

7.2 Waarom een OOB read een arbitrary read is

nameList is een vector van kj::String. Een kj::String is 24 bytes, inclusief een pointer (ptr). Door de OOB-index leest kj::str() 24 bytes na de vector en behandelt dit als een kj::String, waarna de pointer wordt gedereferenceerd. Als we het geheugen na nameList controleren, controleren we de ptr en kunnen we bytes lezen van elk gewenst adres → arbitrary read.

7.3 Opmerkingen

  • De Ada-versie triggerde in productie op Cloudflare.
  • De volledige cross-tenant exploit werd gedemonstreerd op de self-hosted versie, omdat productie een extra check bevat die de open-source build mist.

8. zlib deflateParams() Use-After-Free (UAF)

workerd implementeert het node:zlib module in C++. Hierbij ontdekten we een UAF-kwetsbaarheid.

8.1 Dangling buffers

Bij een compressie-aanroep (write) worden buffers tussen JavaScript, de glue-code en de zlib C-library uitgewisseld.

  • In JS is de output buffer reference-counted. Zodra de referentieteller 0 bereikt, wordt het geheugen vrijgegeven door de garbage collector.
  • Echter, in de native laag stelt workerd in setBuffers de pointer stream.next_out in op het adres van de output buffer. De functie write() vergeet deze pointer te wissen na afloop. Hierdoor blijft er een dangling pointer achter naar vrijgegeven geheugen.

8.2 Het "Use" in Use-After-Free

De pointer wordt niet gebruikt tijdens normale compressie (omdat write hem altijd reset), maar wel in deflateParams. Deze functie, aangeroepen via handle.params() om compressieniveaus aan te passen, flusht eerst eventuele resterende data via de (stale) next_out pointer voordat nieuwe instellingen worden toegepast.

8.3 ZNOFLUSH

Door de flush-mode ZNOFLUSH te gebruiken bij een write-aanroep, wordt data in de interne buffer van zlib gehouden. Deze resterende data is precies wat deflateParams vervolgens probeert te flushen naar het vrijgegeven geheugen.

9. HTMLRewriter AttributesIterator UAF

HTMLRewriter stelt Workers in staat om HTML-tags on-the-fly aan te passen via C++ bindings bovenop de Rust-library lol-html.

De bug zit in de bindings: wanneer een attributes iterator wordt opgevraagd, wordt een raw pointer naar het interne attribuut-array gebruikt. Als men vervolgens met setAttribute nieuwe attributen toevoegt, kan het array groeien en heralloceren naar een nieuwe locatie. De iterator wijst echter nog steeds naar het oude, vrijgegeven array → UAF.

10. KV SQL bypass → arbitrary deserialization

Deze kwetsbaarheid leidt tot willekeurige deserialisatie via Durable Objects.

10.1 Durable Objects

Durable Objects bieden persistente state op de edge, gebruikmakend van een SQLite backend. Data kan worden benaderd via:

  1. De key/value API (gebruikt het structured-clone algoritme voor serialisatie).
  2. De SQL API (voert raw SQL uit).

10.2 De authorizer bypass

Een SQL authorizer voorkomt dat queries interne tabellen (zoals cfKV) aanraken. Echter, de authorizer controleert alleen de referentie van de query, niet de bestemmingsnaam van een hernoeming (RENAME).

Een aanvaller kan een normale tabel maken, deze vullen met kwaadaardige bytes en deze vervolgens hernoemen naar cfKV:

CREATE TABLE kv_tmp (key TEXT, value BLOB); -- toegestaan
INSERT INTO kv_tmp VALUES ('k', <payload>); -- crafted payload
ALTER TABLE kv_tmp RENAME TO _cf_KV;       -- niet gecontroleerd door authorizer

Een daaropvolgende storage.get('k') aanroep voert deze bytes direct in de interne V8 deserializers van workerd, wat kan leiden tot type-confusie en memory corruption.

---

Deel III – De volledige keten en impact

11. Cross-tenant geheimendiefstal (Workers)

Omdat verschillende tenants hun Workers delen in één proces en één native heap (tcmalloc), is de URLPattern OOB read een methode om geheugen van andere tenants te lezen.

11.1 De strategie

De primitieve uit §7 leest 24 bytes na nameList en behandelt dit als een kj::String { ptr, size, disposer }. Door controle over deze bytes te krijgen, kunnen we elke willekeurige locatie in het proces lezen.

11.2 ASLR omzeilen

Om ASLR te verslaan, moeten we een echt heap-adres lekken. We gebruiken de tcmalloc free list: de eerste 8 bytes van een vrijgegeven chunk bevatten een next-pointer naar een andere vrije chunk. Door een chunk vlak na nameList te alloceren, een kleine size te schrijven en deze vervolgens vrij te geven, kunnen we via de OOB read de heap-base lekken.

11.3 Herhaalbare leesacties via VFS files

Om het geheugen systematisch te scannen zonder telkens de heap opnieuw vorm te geven, gebruiken we het Virtual File System (VFS). De inhoud van een VFS-bestand is een native kj::heapArray op de tcmalloc heap. Door een VFS-bestand vlak na nameList te plaatsen, kunnen we de bytes in dat bestand aanpassen om telkens nieuwe adressen te lezen via exec().

11.4 Geheimen van andere Workers lezen

Door de heap te scannen op specifieke patronen (zoals Bearer sk... API tokens), kunnen we geheimen van co-located Workers stelen.

12. Sandbox escape: van zlib UAF naar host RCE

Deze aanval start in Code Mode en eindigt bij native code execution op de host via de zlib Use-After-Free (§8).

12.1 De primitieve verbeteren

We gebruiken de UAF write om een object te overschrijven. Twee verbeteringen maken dit precies:

  1. Offset van de write: Via output offset kunnen we bepalen waar in het hergebruikte object zlib begint te schrijven.
  2. Grootte van de write: We houden de flush klein (8 bytes) om exact één veld te overschrijven zonder het omliggende geheugen te corrumperen.

12.2 Van UAF naar herhaalbare read/write

We richten ons op een "strong object": een VFS file. We overschrijven de metadata (FileImpl) van het bestand, specifiek het veld data.size. Door dit veld te vergroten, wordt de normale bounds check van het bestand irrelevant en kunnen we via de file API lezen en schrijven buiten de grenzen van de buffer → OOB read/write over de heap.

12.3 Arbitrary read/write

Door een tweede FileImpl object te gebruiken, kunnen we diens data.ptr overschrijven met elk gewenst adres in het proces. Dit geeft ons een stabiele arbitrary 64-bit read/write primitieve.

12.4 Naar native code

In de self-hosted build is de V8 sandbox uitgeschakeld, wat de finale stap triviaal maakt. workerd reserveert een RWX (Read-Write-Execute) regio op een vast adres (0xaaaaf0000000).

De finale stappen:

  1. Gebruik arbitrary write om ARM64 shellcode (een reverse shell) in de RWX-regio te plaatsen.
  2. Kap een functiepointer binnen de zlib stream (write callback) en richt deze naar de shellcode.
  3. Roep handle.write() aan, waardoor de controle springt naar de shellcode op de host, volledig buiten de V8 isolate.

Opmerking: Deze specifieke techniek werkt alleen als de V8-cage is uitgeschakeld. Met de cage ingeschakeld zou een ander pad naar RCE nodig zijn.

---

Deel IV – Takeaways en Disclosure

13. Defensieve lessen

  • De engine is niet de volledige grens. Hardening van V8 en de "cage" is noodzakelijk, maar onvoldoende. Elke native API die bereikbaar is vanuit JS maakt deel uit van de beveiligingsgrens.
  • Glue-layers verdienen prioriteit bij security reviews. De JSG-laag beheert lifetimes en pointers; dit is precies waar UAFs en ontbrekende bounds checks ontstaan.
  • Native allocaties vereisen een eigen threat model. tcmalloc free-lists en VFS-buffers liggen buiten de kooi. Als de kooi je isolatieverhaal is, zijn de dingen die hij niet bestrijkt je aanvalsoppervlak.
  • Agent-gegenereerde code is normale code. In Code Mode is het schrijven van exploit-achtige TypeScript geen uitzondering, maar de bedoelde werking. Prompt injection moet worden gemodelleerd als een entry point voor code-execution.

Disclosure tijdlijn

Alle vijf de kwetsbaarheden zijn via HackerOne gemeld aan Cloudflare onder gecoördineerde disclosure.

DatumGebeurtenis
1 februari 20264 van de 5 kwetsbaarheden gemeld (zlib UAF, HTMLRewriter UAF, URLPattern OOB reads)
11 maart 2026Cloudflare classificeert twee als Kritiek (zlib UAF, HTMLRewriter UAF)
12 maart 2026De 5e kwetsbaarheid gemeld (KV SQL-bypass → deserialisatie)
5–6 augustus 2026Publieke onthulling tijdens Black Hat USA 2026

Reacties en bevestigingen van Cloudflare

  • Classificatie: Twee kwetsbaarheden (zlib UAF en HTMLRewriter UAF) zijn als Kritiek beoordeeld.
  • Bereik in productie: Cloudflare bevestigde dat de bugs reproduceerbaar zijn in productie, met uitzondering van de originele urlpattern_original read (de Ada-versie is wel kwetsbaar).
  • De kooi en de heap: Cloudflare bevestigde dat de tcmalloc native heap buiten zowel de V8 sandbox (cage) als de memory-protection keys valt.
  • Fix: Beheerde Workers zijn in productie hersteld. Voor self-hosted implementaties sluit workerd v1.20260619.1 alle bugs. Er zijn tot op heden geen CVE's toegewezen.