SSE-redactie: Systeemprompts van LLM's verdedigen in streaming-architecturen
Door Better ISMS — februari 2026
Als je een product bouwt bovenop een LLM, dan is je systeemprompt je productlogica. Wanneer iemand die extraheert, krijgen ze je redeneringen, je vangrails, je concurrentievoordeel — alles. En als je reacties streamt via Server-Sent Events (wat je waarschijnlijk doet), is het verdedigen tegen extractie moeilijker dan je denkt.
Dit bericht beschrijft SSE-redactie, een techniek die we voor ISMS Copilot hebben gebouwd om lekken van systeemprompts midden in de stream te detecteren en te neutraliseren. We delen de architectuur zodat anderen die LLM-producten bouwen iets soortgelijks kunnen implementeren.
Het probleem
De meeste LLM-applicaties streamen reacties chunk voor chunk naar de client met behulp van SSE. Elke chunk wordt verzonden op het moment dat deze wordt gegenereerd. Er is geen stap om de "volledige reactie te controleren voor verzending" — dat zou het doel van streaming tenietdoen.
Dit creëert een beveiligingslek: als een jailbreak-prompt het model overtuigt om zijn systeeminstructies te dumpen, vliegen de gegevens al naar de client voordat je het kunt stoppen. Tegen de tijd dat je beseft wat er is gebeurd, heeft de gebruiker al honderden of duizenden tekens van je systeemprompt gezien.
Traditionele outputfiltering werkt hier niet. Je kunt niet de hele reactie bufferen (latentie verpest de gebruikerservaring) en je kunt niet elke kleine chunk afzonderlijk controleren (een fragment van 5 woorden ziet er niet uit als een systeemprompt).
Voor algemene strategieën ter voorkoming van jailbreaks, zie Mitigate Jailbreaks and Prompt Injections. SSE-redactie is een defense-in-depth maatregel voor situaties waarin die preventies falen.
De architectuur
SSE-redactie werkt in vier fasen.
Fase 1 — Fingerprinting. Voordat er een gesprek plaatsvindt, extraheer je een set vingerafdruk-frases uit je systeemprompt. Dit zijn kenmerkende reeksen die alleen samen zouden verschijnen als het model zijn instructies reproduceert. Je wilt frases verspreid over verschillende secties van je prompt — roldefinities, namen van beperkingen, gedragsregels. Het aantal vingerafdrukken en de drempelwaarde voor overeenkomst zijn geheime parameters die je naar wens kunt aanpassen.
Fase 2 — Accumulatie en periodieke controle. Terwijl het model chunks streamt, verzamelt een guard de volledige tekst van de reactie. Met regelmatige tussenpozen (gemeten in aantal tekens, niet per chunk) wordt de verzamelde inhoud gecontroleerd tegen de set vingerafdrukken. Het controleren van elke chunk zou verspilling zijn — de vingerafdrukken hebben genoeg omringende context nodig om een zinvolle match te geven.
Fase 3 — Foutpropagatie. Wanneer de guard voldoende overeenkomsten met vingerafdrukken detecteert, gooit deze een getypeerde fout (in ons geval SystemPromptLeakError). Dit is waar de subtiliteit zit. In een streaming-architectuur heeft de chunk-verwerkingslus meestal een try/catch voor het afhandelen van misvormde SSE-data (slechte JSON, onverwachte formaten). Dat generieke catch-blok zal je beveiligingsfout inslikken als je niet voorzichtig bent. Je hebt een guard clause nodig die jouw specifieke fouttype opnieuw gooit voordat de generieke handler wordt uitgevoerd:
catch (e) {
if (e instanceof Error && e.name === 'SystemPromptLeakError') throw e;
// generic error handling continues for everything else
}Dit is een oneliner, maar zonder dit is het hele detectiesysteem inactief. De guard gaat af, logt de detectie, en de stream gaat vrolijk door met het afleveren van je systeemprompt aan de aanvaller. We hebben dit op de harde manier geleerd — onze guard detecteerde lekken perfect in de logs, terwijl hij absoluut niets deed om ze te stoppen.
Fase 4 — Redactie. Zodra de fout is doorgegeven aan de stream-controller, stuurt deze een redact SSE-event naar de client. De client vervangt alles wat al was gerenderd door een weigeringsbericht. De server vervangt tegelijkertijd de opgeslagen inhoud in de database, zodat het lek niet blijft bestaan.
Wat de gebruiker ziet
De aanvaller ziet kortstondig een deel van de gestreamde inhoud — misschien voor een paar seconden — en vervolgens wordt de volledige reactie vervangen door een generieke weigering. De ervaring is: tekst verschijnt, verdwijnt dan en wordt vervangen. De gedeeltelijke inhoud die ze hebben opgevangen is onvolledig en vermengd met normale reactietekst, waardoor deze onbetrouwbaar is voor extractie.
Lees meer over hoe weigeringsberichten werken in Handle Refusals and Scope Limits.
Het probleem met het catch-blok
Dit verdient extra nadruk omdat het het soort bug is dat elke test doorstaat, maar faalt in productie.
Als je async generators gebruikt voor streaming, ziet je SSE-parsing-lus er waarschijnlijk zo uit:
for (const line of sseLines) {
try {
const data = JSON.parse(line);
const text = extractText(data);
await onChunkCallback(text); // <-- guard runs here
yield text;
} catch (e) {
console.error('Error parsing chunk:', e);
// continues to next line
}
}De callback bevindt zich in het try-blok. Als de guard een fout gooit, logt de catch dit als een parseerfout en gaat verder. In ons geval detecteerde de guard het lek correct bij elke chunk nadat de drempel was bereikt — de logs lieten zien dat SystemPromptLeakError herhaaldelijk afging — terwijl de stream normaal werd voltooid, de volledige gelekte prompt in de database werd opgeslagen en naar de client werd verzonden.
De extra complicatie: dit gedrag is afhankelijk van de runtime. In Node.js kunnen fouten van async generators uit callbacks anders propageren dan in Deno. Onze tests slaagden in de Node.js testomgeving omdat de fout toevallig propageerde. In de Deno-productieomgeving werd deze ingeslikt. Als je dit bouwt, test dan in je werkelijke productie-runtime, niet alleen in je test-runner.
Noemenswaardige ontwerpbeslissingen
Waarom vingerafdrukken in plaats van embedding similarity of exacte overeenkomsten? Vingerafdrukken zijn snel (string matching), deterministisch (geen model-aanroepen) en robuust tegen parafraseren. Het model parafraseert zelden zijn eigen systeemprompt tijdens een lek — het reproduceert deze letterlijk of bijna letterlijk. Embedding similarity voegt latentie toe per controle en introduceert het risico op fout-positieven bij legitieme nalevingsinhoud. Exacte substring-matching is te kwetsbaar (spaties, verschillen in opmaak).
Waarom periodiek controleren in plaats van elke chunk? Chunks zijn klein (vaak 3–10 tekens). Een enkele chunk is betekenisloos voor detectie. Door te accumuleren tot een minimumdrempel voordat er wordt gecontroleerd, verminder je de benodigde rekenkracht en zorg je voor voldoende context voor een betrouwbare match.
Waarom niet de volledige reactie bufferen? Bufferen verpest de streaming-gebruikerservaring. Gebruikers verwachten tekst in realtime te zien verschijnen. Een buffer van 2 seconden is merkbaar; het bufferen van een volledige reactie van meer dan 4000 tekens is onacceptabel. SSE-redactie behoudt realtime streaming voor 99,99% van de gesprekken en grijpt alleen in bij een actief lek.
Waarom ook in de database vervangen? Als je alleen aan de client-zijde redigeert, blijft de gelekte inhoud aan de server-zijde bestaan. Iedereen met toegang tot de database, elke exportfunctie of elk endpoint voor gespreksgeschiedenis zou de tekst alsnog blootstellen.
Wat dit niet oplost
SSE-redactie is een defense-in-depth maatregel, geen wondermiddel.
Het voorkomt niet dat het model probeert te lekken. Dat is wat de instructies in je systeemprompt zelf afhandelen (expliciete weigeringsinstructies, secties met beperkingen). SSE-redactie is het vangnet voor wanneer die instructies falen — en met genoeg creativiteit slagen jailbreaks af en toe.
Het voorkomt geen lekken die korter zijn dan de detectiedrempel. Als iemand het model zover krijgt dat het één zin van de systeemprompt onthult, zal het aantal vingerafdrukken de drempel niet halen. Dit is met opzet — je maakt een afweging tussen het vangen van volledige extracties (hoge betrouwbaarheid) en het markeren van gedeeltelijke vermeldingen (hoog risico op fout-positieven).
De aanvaller ziet wel gedeeltelijke inhoud vóór de redactie. Gedurende een paar seconden is gestreamde tekst zichtbaar. Dit is inherent aan streaming-architecturen. De gedeeltelijke inhoud is onvolledig en mist structuur, maar er is wel sprake van enige blootstelling.
SSE-redactie is een aanvulling op, maar geen vervanging voor best practices voor de beveiliging van systeemprompts. Zie System Prompts en Protect Workspace and Custom Instructions voor fundamentele beveiligingsmaatregelen.
Checklist voor implementatie
Als je dit wilt bouwen voor je eigen LLM-product:
Extraheer vingerafdruk-frases uit je systeemprompt — kies kenmerkende strings die verschillende secties beslaan.
Bouw een guard die gestreamde inhoud verzamelt en periodiek controleert op vingerafdrukken.
Definieer een getypeerde foutklasse met een herkenbare naam voor lekdetectie.
Controleer elk catch-blok in je streaming-pipeline — voeg re-throw guards toe voor jouw fouttype.
Handel de fout af in je stream-controller door een redact-event te sturen en de opgeslagen inhoud te vervangen.
Handel het redact-event op de client af door de gerenderde inhoud te vervangen door een weigeringsbericht.
Test in je productie-runtime, niet alleen in je test-runner.
Houd je vingerafdrukken, drempelwaarden en controle-intervallen geheim.
Afsluitende gedachte
Het moeilijkste deel hiervan was niet het detectie-algoritme — het was een bug van één regel in een catch-blok die het hele systeem geruisloos uitschakelde. Beveiliging in streaming-architecturen faalt vaak op het niveau van de infrastructuur, niet op het niveau van het algoritme. Als je beveiligingsfuncties voor LLM's bouwt, volg dan het volledige foutpad van detectie naar de actie aan de gebruikerskant, en verifieer dit in je werkelijke productieomgeving.
Voor een breder overzicht van AI-veiligheidspraktijken bij ISMS Copilot, zie de AI Safety & Responsible Use Overview.
Better ISMS bouwt compliance-tools voor informatiebeveiligingsteams. ISMS Copilot is onze AI-assistent voor ISO 27001, SOC 2, GDPR en aanverwante kaders.