Een hernieuwde blik op remote Spectre-aanvallen op Cloudflare Workers
In 2021 hebben we remote Spectre-aanvallen tegen Cloudflare Workers geanalyseerd. Op basis van die resultaten hebben we een productiemethode voor verdediging geïmplementeerd, genaamd Dynamic Process Isolation (DyPrIs), die scripts met een verdacht uiterlijk identificeert en deze isoleert in aparte processen. Sinds die tijd zijn er nieuwe technieken ontdekt om Spectre-aanvallen te stabiliseren. Om te begrijpen of deze technieken een bedreiging vormden voor onze productieomgeving van Workers, hebben we besloten de remote Spectre-aanval intern opnieuw te evalueren. Door een bijgewerkte proof-of-concept op de productieomgeving te bouwen, konden we het risico van Spectre-aanvallen onder productiebelastingen empirisch beoordelen.
Om een succesvolle side-channel aanval in productie uit te voeren, moet een externe aanvaller aanvullende obstakels overwinnen, zoals activiteit op gedeelde hardwarebronnen, interrupts, contextswitches en grofmazige timers. Ons onderzoek bracht een beperking aan het licht in de implementatie van DyPrIs. We slaagden erin een remote Spectre-aanval te demonstreren die betrouwbaar tot 12 bit/s lekte met een nauwkeurigheid van 99% in de productieomgeving van Cloudflare Workers. Als gevolg van dit onderzoek hebben we DyPrIs verbeterd en de V8 Sandbox en een in-proces isolatiemechanisme geïntegreerd om het risico op geheugenlekken verder te verminderen.
Vandaag publiceren we een paper waarin onze bevindingen worden beschreven, co-auteurs zijn Albert Pedersen, Haocheng Xiao, Sam Ainsworth, Nigel Topham en Martin Schwarzl. Deze paper behandelt onderzoek dat is uitgevoerd in 2024 en begin 2025.
Let op: de gepresenteerde aanval is inmiddels gemitigeerd in het productiesysteem door tegenmaatregelen die zijn toegepast door het Cloudflare Workers Runtime-team. We hebben de afgelopen drie jaar geen indicatoren gevonden van actieve exploitatie.
Het beveiligingsmodel van Cloudflare Workers
Cloudflare Workers draait onbetrouwbare JavaScript op de edge. Door gebruik te maken van isolatie op taalniveau, in de vorm van V8-isolaten, kunnen tienduizenden tenants hetzelfde besturingssysteemproces delen. Elke Worker heeft zijn eigen aparte JavaScript-heap. Dit ontwerp houdt de opstartlatentie laag en stelt ons in staat om veel tenants zeer efficiënt te draaien in vergelijking met volledige procesisolatie.
Rondom de runtime hebben we meerdere lagen verdediging, zoals:
- Geautomatiseerde V8-patchpijplijnen;
- Een tweelaagse sandbox bestaande uit Linux-namespaces en seccomp-filters;
- Cap’n Proto RPC;
- De mogelijkheid om bepaalde scripts in aparte proces-sandboxes te plannen.
Toch kan een enkele willekeurige leeskwetsbaarheid binnen een Worker-proces leiden tot lekken tussen tenants. Een kwetsbaarheid die zeer moeilijk te mitigeren is, maakt gebruik van de aard van speculatieve uitvoering: in-proces Spectre.
Spectre
Speculatieve uitvoering kun je vergelijken met wandelen. Op een bepaald punt kom je bij een splitsing en moet je voorspellen welke kant je op moet. Als de voorspelling correct was, heb je tijd bespaard en kun je genieten van de zon en een verfrissend drankje in een berghut. Als je echter de verkeerde kant op speculeert, moet je omkeren. Het pad ziet er ongeschonden uit, maar je voetstappen blijven zichtbaar in de modder.
Speculatieve uitvoering in CPU's werkt op een vergelijkbare manier. De branch prediction doet vooraf een gefundeerde gok over de uitkomst van een vertakking en de CPU voert deze speculatief uit. Als de voorspelling correct was, heeft de speculatieve uitvoering tijd bespaard. Als de voorspelling echter onjuist is, moet de CPU de resultaten weggooien, terugdraaien en de andere vertakking uitvoeren. Omdat deze speculatief uitgevoerde instructies slechts tijdelijk in de CPU-pijplijn bestaan en nooit permanent worden definitief gemaakt of gecommit, noemt de literatuur ze transient instructions en wordt het concept gegeneraliseerd als transient execution.
Vanwege deze tijdelijke uitvoering blijven er echter sporen achter in de microarchitecturale status, bijvoorbeeld in de CPU-caches. Een aanvaller kan Spectre dus gebruiken om tijdelijk toegang te krijgen tot geheugen buiten de grenzen (out of bounds), één bit aan informatie in de cachestatus coderen en de latentie van het opnieuw benaderen van gegevens gebruiken om af te leiden of de bit was ingesteld of niet.
Om in-proces Spectre-aanvallen te mitigeren, bevriest Cloudflare Workers lokale timers, verbiedt multithreading en gedeeld geheugen, detecteert dit actief, husselt periodiek het geheugen en isoleert scripts die er kwaadaardig uitzien in aparte processen.
Aanvalsprimitieven
Een aanvaller heeft een remote timer nodig om te meten hoe lang het duurt om te testen of een tijdelijk gelekte bit een '0' of '1' is.
Het Cloudflare Workers-platform beperkt timers bewust. Tijdens CPU-only uitvoering is de tijd effectief bevroren. Date.now() en performance.now() bieden geen continu voortschrijdende klok met een hoge resolutie. Er is geen gedeeld geheugen en geen multithreading, waardoor de klassieke counter-thread timer via een SharedArrayBuffer niet beschikbaar is.
Om een aanval succesvol uit te voeren, moeten verschillende uitdagingen worden opgelost:
- De Workers-runtime is beperkt en co-locatie tussen een aanvaller en een slachtoffer moet worden gegarandeerd.
- Er moet een betrouwbare, idealiter co-gelokaliseerde remote timer worden ontdekt die stabiele timingmetingen mogelijk maakt.
- De aanval draait onder productieomstandigheden, wat extra stabiliteitsmaatregelen vereist, zoals een betrouwbare Spectre-gadget die tijdelijke 64-bit out-of-bounds toegang mogelijk maakt, robuuste signaalversterking om om te gaan met systeem- en netwerkruis, en een primitief om gegevens betrouwbaar uit de cache te verwijderen (evict).
Spectre-gadget
return probeArray[
obj instanceof ObjP
? PROBEARRAY_OFFSET + ((obj.ptr[0] >> bit) & 1) * 0x800
: 0x400
];
Speculatieve type-verwarring Spectre-gadget
Met de juiste Spectre-gadget (zie bovenstaand fragment) kan een aanvaller tijdelijk toegang krijgen tot geheugen buiten de grenzen en één bit in de cache (probeArray) coderen. De aanvaller meet vervolgens de latentie van de geheugentoegang om te bevestigen of gegevens zijn gecached of niet. Een snellere toegang betekent dat de lijn is gecached en de bit 1 was. Omgekeerd betekent een tragere toegang dat deze niet was gecached en de bit 0 was.
In onze aanval gebruiken we twee verschillende soorten Spectre-gadgets:
- De eerste lekt gecomprimeerde heap-pointers, bijvoorbeeld het heap-basisadres (root) van het isolaat.
- De tweede maakt gebruik van een speculatieve type-verwarring (type confusion) om te lekken vanaf een willekeurige, door de aanvaller gemaakte 64-bit userspace-pointer.
Ten tijde van het onderzoek was de V8 Sandbox nog niet geïmplementeerd bij Cloudflare Workers. Onder pointer-compressie gebruiken de meeste objecten 32-bit gecomprimeerde pointers. TypedArray was een van de weinige uitzonderingen die nog steeds een ruwe 64-bit pointer naar zijn backing store bewaarde, wat precies is waar onze gadget misbruik van maakt.
De vertakking obj instanceof ObjP voert een typecontrole uit. Om de branch prediction foutief te trainen (mistrain), roepen we de gadget vele malen aan op echte ObjP-instanties, en vervolgens op een ander object met een door de aanvaller gecontroleerde geheugenindeling ObjI. De CPU speculeert over de genomen vertakkingen en volgt obj.ptr[0], ook al heeft het object een ander type. Om één bit te lekken, maskeren we één bit en gebruiken we deze om een van de twee probeArray-lijnen te selecteren.
Door de heap-lekkagegadget te exploiteren, brengen we naburige objecten in kaart en lokaliseren we een door de aanvaller gecontroleerde array. Onze tweede gadget verwart twee grote objecten die over meerdere cachelijnen verspreid zijn, zodat het typeveld op een andere cachelijn landt dan het veld dat we lezen. Door het typeveld te verwijderen (evicting), wordt het speculatievenster geopend terwijl het doelfeld gecached blijft, en de tijdelijke leesactie volgt een door de aanvaller gecontroleerde 64-bit waarde. Dit verandert het lek in een willekeurige adresleesactie.
Signaalversterking
Een cache-hit en een cache-miss verschillen slechts enkele nanoseconden. Bovendien is een remote timer onnauwkeurig op de schaal van enkele microseconden tot enkele milliseconden. Daarom is een vorm van signaalversterking nodig.
Stephen Röttger en Artur Janc ontdekten een manier om een enkele geheugentoegang te versterken door misbruik te maken van het boomgebaseerde pseudo least recently used (PLRU) cache-vervangingsbeleid in L1-caches. PLRU organiseert elke cacheset als een binaire boom waarvan de knopen wijzen naar de kant die het minst recent is gebruikt. Met het juiste toegangspatroon kan een aanvaller een doellijn onbepaald gecached houden door de boombuur ervan aan te raken telkens wanneer de pointers naar het doel wijzen.
Door dit gedrag te benutten, kan de timing van een enkel cache-event willekeurig worden versterkt, zodat het leidt tot veel L1-hits (sneller) vergeleken met veel L1-misses in het tegenovergestelde geval.
Remote timer
Zolang het signaal kan worden versterkt, is een onnauwkeurige remote timer voldoende om een gecodeerde bit te onderscheiden. Bijvoorbeeld, een WebSocket-verbinding met een externe server die timestamps met een hoge resolutie serveert, is voldoende. De timer kan worden gehost bij Cloudflare of in een datacenter dat co-gelokaliseerd is met het datacenter waar de Worker draait. De Worker vraagt de remote timer om een tijdstempel te markeren voor een bepaald event en berekent het delta voor een ander verzoek zodra het event is gestopt.
In de paper evalueerden we verschillende timer-opstellingen en konden we betrouwbaar resoluties onder de ms bereiken op de mediaan, zelfs over grotere topologische afstanden.
Herhaalbare metingen
Een enkele meting is niet genoeg. Productiemachines zijn onrustig (noisy), dus moet een aanvaller elke meting minimaal een paar keer herhalen en een statistische discriminator gebruiken. Herhaling betekent in ons geval het resetten van de cachestatus. Twee dingen moeten vóór elke ronde worden verwijderd:
- De waarde waar de speculatieve vertakking van afhangt, zodat de resolutie van de vertakking lang genoeg stagneert om een speculatievenster te openen.
- De probe-lijn die de gelekte bit codeert, zodat de volgende tijdelijke toegang deze opnieuw kan cachen.
Aangezien er geen directe instructie beschikbaar is in JavaScript, is de klassieke manier om dit te doen het bouwen van een eviction set: een groep adressen die naar dezelfde cacheset mappen als het doel.
Dougall Johnson beschreef een elegantere methode gebaseerd op het duiventilprincipe (pigeonhole principle): als je veel meer data alloceert dan de cache kan bevatten, is een willekeurig gekozen cachelijn bijna zeker niet gecached. Voor een 256 KB L2-cache laat het alloceren van 64 MB maximaal een kans van 1/256 over dat een willekeurige cachelijn nog in L2 zit. In plaats van een specifieke lijn te verwijderen, kies je een nieuwe willekeurige locatie die met overweldigende waarschijnlijkheid al is verwijderd.
Om dit in JavaScript te gebruiken, alloceren we een grote pool van paren van aanvaller- en slachtofferobjecten die de last-level cache overschrijdt. Elke meetronde selecteert een nieuw willekeurig paar.
Co-locatie van de aanvaller en het slachtoffer-isolaat
Voor de aanval moeten zowel het aanvaller- als het slachtoffer-isolaat in hetzelfde proces op dezelfde edge-server worden gepland. Hoewel Cloudflare tienduizenden edge-servers exploiteert, is dit in feite vrij eenvoudig op Cloudflare Workers. Omdat Workers zo zijn ontworpen dat ze op elke Cloudflare edge-server kunnen worden uitgevoerd, zal het aanroepen van het slachtofferscript vanuit het aanvallersscript via fetch("https://victim.example") in de meeste gevallen ervoor zorgen dat de scheduler een instantie van de victim worker in exact hetzelfde proces opstart. Het slachtoffer-isolaat kan in leven worden gehouden door met een bepaald interval herhaaldelijk subverzoeken ernaar te sturen.
Bovendien is de stabiliteit van de aanval sterk afhankelijk van de CPU-belasting van de server. Dit stelt een aanvaller in staat om de aanval strategisch uit te voeren in een regio waar het buiten piekuren is (bijv. in een Australisch datacenter tijdens Europese kantooruren).
Het omzeilen van resource-limieten van isolaten
De Cloudflare Workers-runtime handhaaft limieten op alle isolaten om het platform te beschermen en misbruik te voorkomen. Relevant voor deze aanval waren de limieten van 30 seconden CPU-tijd en 1.000 subverzoeken per aanroep.
Voor een reguliere Worker is elk HTTP-verzoek een nieuwe aanroep die deze limieten reset. Het probleem is om sequentiële verzoeken op dezelfde edge-server te landen. Durable Objects lossen dit op. Durable Objects zijn gebouwd voor real-time coördinatie; de runtime behandelt elk binnenkomend WebSocket-bericht als een aanroep die de CPU-tijd en verzoeklimieten reset. De aanvaller opent een persistente WebSocket naar een Durable Object-worker en stuurt regelmatige keep-alive berichten. Dit houdt een enkel isolaat in leven en geeft ons een persistent, bidirectionaal kanaal.
Een bijzonderheid kostte ons tijd: een isolaat is single-threaded, dus binnenkomende WebSocket-berichten worden alleen verwerkt wanneer het script de controle teruggeeft aan de event loop. Tijdens synchrone code ziet de runtime de keep-alive niet. Als de thread langer dan 30 seconden geblokkeerd blijft, doodt de runtime het isolaat. Door regelmatig tussen bursts te yielden, konden we een isolaat vijf tot meer dan 20 uur in leven houden.
Alles samenvoegen
De vorige aanval vertrouwde grotendeels op herhaling om een enkele cachetoegang te versterken en lekte daarom traag (120 bit/u). Wij combineerden boomgebaseerde PLRU-versterking met meetlussen. Elke iteratie creëert de cachestatus opnieuw, wat meer timingverschillen toevoegt.
for (let s = 0; s < SAMPLE_NUM; s++) {
timer.mark("mark S" + s);
for (let r = 0; r < OUTER_REP_NUM; r++) {
setup(); // branch mistraining en cachecontrole
leak(secretBit); // tijdelijke toegang
PLRU(cacheSet, INNER_REP); // versterken
}
timer.mark("mark E" + s);
}
delta = fetchFromServer(SAMPLE_NUM);
return median(delta);
We hebben de volledige end-to-end aanval gedemonstreerd in de productieomgeving van Cloudflare Workers tegen Workers die we zelf controleerden.
- We stelden co-locatie vast tussen een aanvaller-Worker, een slachtoffer-Worker en een remote timer. Durable Objects zorgden voor een langdurige uitvoeringscontext.
- We voegden een kalibratiestap toe om de timer te testen met speculatief bereikbare waarden. Dit is belangrijk omdat productiemachines onrustig zijn.
- Als eerste stap lekten we de isolaat-root uit één Worker en gebruikten we in een andere Worker de speculatieve type-verwarring met 64-bit pointers om uit de isolaat-root te lezen.
- Als tussenstap bevestigden we 64-bit lekkage door geheugen uit de vDSO-regio te lezen (die leesbare strings bevat zoals
gettimeofday). - Ten slotte plaatsten we een JWT-token in de slachtoffer-Worker en lekten we dit bit voor bit.
In productie bereikten we een leaksnelheid van maximaal 12 bit/s met een nauwkeurigheid van meer dan 99%.
Robuustheid
Afhankelijk van het tijdstip van de dag varieert de benutting van een machine sterk. Dit vertraagt de aanval omdat er meer data moet worden gesampled. Toch blijft de aanval zelfs bij een hoge CPU-benutting haalbaar.
Waarom werd dit niet gedetecteerd?
DyPrIs houdt hardware-prestatietellers in de gaten en isoleert een script in zijn eigen proces zodra het lijkt op een Spectre-aanval. Twee zaken hielden de aanval onder de radar:
- DyPrIs isoleert een script pas nadat de aanroep is beëindigd. De keep-alive truc met Durable Objects kan uren tot een dag duren. De leak is voltooid lang voordat isolatie zou optreden.
- DyPrIs normaliseert vertakkingsfouten (branch mispredictions) door het aantal iTLB-toegangen. Onze remote timer is één grote I/O-lus, en dat WebSocket-verkeer blaast de iTLB-activiteit op. De genormaliseerde ratio zakt onder de detectiedrempel, waardoor de aanval lijkt op een gewone I/O-intensieve Worker.
Wat we hebben aangepast
We richten ons op drie gebieden: voortdurende V8-harding, sterkere in-proces isolatie en verbeterde detectie.
V8 Sandbox
Het uiteindelijke doel van de V8-geheugensandbox is om ruwe 64-bit pointers uit grote delen van de JavaScript-heap te verwijderen. Dit maakt de specifieke speculatieve type-verwarring-gadgets in dit werk moeilijker te hergebruiken, omdat typed-array backing stores niet langer dezelfde ruwe pointerstructuur blootstellen. De sandbox is echter geen volledige Spectre-mitigatie.
Hardware-ondersteunde in-proces isolatie
In september 2025 hebben we in-proces isolatie voor Workers geïmplementeerd met behulp van Memory Protection Keys (MPK). MPK stelt een proces in staat om geheugen te verdelen in beschermingsdomeinen en toegangsrechten goedkoop te wisselen. Elke heap zit nu achter een door hardware afgedwongen toegrens. Een geheugentoegang tot een pagina die is beschermd met de verkeerde sleutel wordt door de hardware geweigerd. Dit blokkeert de directe cross-isolaat heap-leesactie waar dit onderzoek op vertrouwde.
Verbeterde DyPrIs
We hebben DyPrIs verbeterd zodat langdurige uitvoeringen en I/O-intensieve workloads als primaire beveiligingsgevallen worden behandeld. Detectie kan niet pas gebeuren nadat een script is beëindigd.
We onderzoeken momenteel of remote timing-gedrag kan worden toegevoegd als extra dimensie aan DyPrIs. De betere aanpak is om herhaalde timer-achtige I/O rondom rekenintensieve secties te behandelen als onderdeel van het gedragssignaal, en niet als achtergrondruis.
Groetjes,