Werkplaats / De werkplaats
In jouw eigen tempo
← AI-security & red teaming
MODULE 01 · LES 02

Toon en begrens directe instructieoverschrijving

Bouw een kwetsbare kleurmachine en houd daarna de vaste kleur in vertrouwde applicatiecode, met exacte bronherkomst.

75 min3 checkpoints
DIT GA JE ONTDEKKEN
  • Je herkent letterlijke kleurinstructies met correcte codepuntposities en bronherkomst.
  • Je bewijst een directe overschrijving en een begrensde verdediging zonder echte-LLM-garantie.

Een oefenassistent moet altijd een blauw kaartje tonen. Je typt een verzoek om een groen kaartje, en de kleur verandert. Er is geen geheim gestolen, maar wel een regel overschreven: precies dat kleine effect gaan we zichtbaar maken.

Directe promptinjectie (direct prompt injection) komt binnen via de gebruikersinvoer zelf. Een gebruiker kan bewust een grens proberen te verschuiven, of onbedoeld conflicterende tekst plakken. We gebruiken een vaste nep-LLM met drie onschuldige kleuren, zodat je oorzaak en gevolg exact kunt volgen.

1. Geef de oefenmachine een zichtbare taal

Onze machine herkent uitsluitend de letterlijke tokens [[kleur:blauw]], [[kleur:groen]] en [[kleur:oranje]]. Hoofdletters, andere kleuren en gewone zinnen zijn geen nieuwe instructies in deze grammatica. Dat maakt de proef controleerbaar; een echt taalmodel begrijpt veel meer soorten tekst en reageert niet zo voorspelbaar.

Bekijk ieder gevonden token als een gemarkeerd stukje papier. Je bewaart de kleur, de beginpositie en de positie direct ná het token. Zo kun je later aanwijzen welke invoer de uitkomst beïnvloedde.

In de formule zijn s en e begin en einde, en ℓ is de lengte. Het interval heeft het begin wel en het einde niet inbegrepen. In Python kun je dezelfde tekens terugvinden met de slice text[s:e].

ℓ=e−s

De emoji hieronder telt als één Python-codepunt; daarna komt een spatie. Het groene token begint dus bij twee en eindigt bij zeventien: vijftien codepunten. Verwar die posities niet met bytes of de UTF-16-eenheden van een andere programmeertaal.

text = "🙂 [[kleur:groen]]"
found = find_markers(text)
print(found[0]["start"],
      found[0]["end"])
print(found[0]["color"])
# 2 17
# groen

Je kunt de drie tokens letterlijk per positie vergelijken, of re.finditer gebruiken. Bij een reguliere expressie moeten de vierkante haken letterlijk worden herkend; match.start() en match.end() leveren de gevraagde grenzen. Verzamel alle treffers in tekstvolgorde.

01
CHECKPOINTVind de letterlijke kleurinstructies

2. Bouw de kwetsbare keuze

De systeemtekst bevat precies één herkend kleurtoken. De kwetsbare machine leest daarna de gebruikersinvoer en kiest daarvan de laatste kleur, als er een is. Zonder gebruikerstoken blijft de systeemkleur staan.

Noem de systeemkleur c met index S. De gebruikerskleuren zijn u met indices één tot en met n; c met index N is de naïeve uitkomst. De twee rijen beschrijven wat er gebeurt met en zonder gebruikerstokens.

cN={unn>0cSn=0
system = "[[kleur:blauw]]"
user = "Vat samen. [[kleur:groen]]"
reply = naive_reply(system, user)
print(reply["color"])
print(reply["origin"]["kind"])
# groen
# user

De herkomst maakt de fout zichtbaar: de uitkomst kwam uit user, hoewel de applicatieregel de systeemkleur bedoelde. Het is niet genoeg om alleen te kijken of een kleur geldig is. Groen behoort tot onze toegestane woordenschat, maar mag deze vaste regel niet vervangen.

02
CHECKPOINTBouw de kwetsbare kleurmachine
VERKEN HET ZELFWelke bron kiest de kleur?

De naïeve regel volgt het laatste herkende gebruikerstoken. De verdedigde regel houdt de oefenkleur in vertrouwde applicatiecode.

Systeembericht

[[kleur:blauw]]

blauw [0, 15)

Gebruikersbericht

[[kleur:groen]]

groen [0, 15)

Posities [start, einde) tellen Python-codepunten. De emoji telt als één. Alleen exacte kleine-lettertokens tellen; fragmenten uit verschillende berichten worden niet samengeplakt.

Naïef

groen

Gekozen uit: gebruikersbericht.

Verdedigd

blauw

Gekozen uit: systeembericht.

Deze verdediging geldt voor onze vaste kleurgrammatica. Alleen een systeemrol of bronlabel toevoegen geeft geen sluitende bescherming voor een echt taalmodel.

Eigen, vooraf berekende oefengegevens. Je bedient een begrensd model; er worden geen externe systemen, documenten of tools aangesproken.

Een tweede gebruikerstoken wint van het eerste. Dat is een gekozen eigenschap van deze nep-LLM, geen algemene wet over echte modellen. Houd berichten apart: twee halve tokens aan verschillende kanten van een berichtgrens vormen samen geen nieuw token.

user = ("[[kleur:groen]] "
        "[[kleur:oranje]]")
reply = naive_reply(system, user)
print(reply["color"])
# oranje

3. Leg de regel buiten het model vast

De verdedigde functie laat vertrouwde applicatiecode de kleur beheren. Zij controleert beide invoerteksten, maar gebruikt uitsluitend de systeemkleur voor de uitkomst. De gebruikersinvoer kan die specifieke keuze niet overschrijven.

c met index G is de verdedigde kleur. De gelijkheid geldt voor onze beperkte functie: de systeemkleur blijft de uitkomst. Dit is geen extra vriendelijk geformuleerde prompt, maar een expliciete programmaregel.

cG=cS
reply = guarded_reply(system, user)
print(reply["color"])
print(reply["origin"]["kind"])
normal = guarded_reply(
    system, "Wat is de vaste kleur?")
print(normal["color"])
# blauw
# system
# blauw
03
CHECKPOINTHoud de systeemkleur in vertrouwde applicatiecode

Een veelgemaakte fout is concluderen dat echte promptinjectie hiermee is opgelost. Een echt LLM kan ook bronlabels verkeerd behandelen; alleen de omringende applicatie kan daadwerkelijk beperken welke rechten en gevolgen bereikbaar zijn. Test daarom later ook die grenzen, niet alleen of één tekst wordt geweigerd.

Wat je nu kunt

  • Je toont een directe overschrijving met een controleerbare kleurmachine.
  • Je bewaart de herkomst van de gekozen kleur en de precieze tokenposities.
  • Je onderscheidt een vaste applicatieregel van een beveiligingsbelofte over een echt model.

Hierna komt de instructie via een document binnen, terwijl de gewone documenttaak moet blijven werken.

Bronnen

Gecontroleerd op 30 oktober 2026: OWASP LLM01 over directe promptinjectie en de grenzen van promptmaatregelen en MITRE ATLAS 2026.10, techniek AML.T0051.000. De kleurgrammatica en voorbeelden zijn eigen oefenmateriaal.

DE VOLGENDE STAPLees documenten zonder hun instructies over te nemen
JOUW EXPERIMENT

Probeer het zelf

Python
main.py

Python start zodra jij code uitvoert.

UITVOER

Je uitvoer verschijnt hier.
Alles rekent in je eigen browser.

Jouw checkpoints0 / 3
01
Vind de letterlijke kleurinstructies

Schrijf find_markers(text). Tekst is exact een Python-str van 0–512 Unicode-codepunten. Herken alleen de hoofdlettergevoelige letterlijke tokens [[kleur:blauw]], [[kleur:groen]] en [[kleur:oranje]]. Andere tekst blijft data. Offsets zijn Python-codepunten, einde exclusief. Geef een nieuwe lijst in vindvolgorde met nieuwe exacte dicts color/start/end, met color als kleurstring en start/end als exacte Python-ints. Geen tokens geeft []. Behoud alle invoer, vormen, volgorde, readonlyflags en Python-/NumPy-RNG, ook bij fouten. Mutabele resultaten zijn nieuw en delen geen containers met invoer, elkaar of resultaten van herhaalde aanroepen. Ongeldige invoer geeft ValueError; de validatiehelpers zijn gegeven. Elke functie werkt zelfstandig als de andere twee nog TODO zijn.

Een hint

Een letterlijke herkenner hoeft geen vrije taal te begrijpen.

02
Bouw de kwetsbare kleurmachine

Schrijf naive_reply(system,user). Tekst is exact een Python-str van 0–512 Unicode-codepunten. Herken alleen de hoofdlettergevoelige letterlijke tokens [[kleur:blauw]], [[kleur:groen]] en [[kleur:oranje]]. Andere tekst blijft data. Offsets zijn Python-codepunten, einde exclusief. System bevat precies één herkend token. User mag nul of meerdere tokens bevatten. Valideer beide teksten. Uitvoer is exact dict met color (exacte kleurstring) en origin (nieuw dict met kind system/user en id=None). Berichten niet samenplakken om nieuwe tokens over een grens te vormen. Kies het laatste herkende token uit user als dat bestaat, anders de systeemkleur. Origin benoemt de gekozen bron. Deze opzettelijk kwetsbare machine doet geen echte model- of toolaanroep. Behoud alle invoer, vormen, volgorde, readonlyflags en Python-/NumPy-RNG, ook bij fouten. Mutabele resultaten zijn nieuw en delen geen containers met invoer, elkaar of resultaten van herhaalde aanroepen. Ongeldige invoer geeft ValueError; de validatiehelpers zijn gegeven. Elke functie werkt zelfstandig als de andere twee nog TODO zijn.

Een hint

Onderzoek de gebruikerskleur pas nadat de vaste systeemkleur geldig is.

03
Houd de systeemkleur in vertrouwde applicatiecode

Schrijf guarded_reply(system,user). Tekst is exact een Python-str van 0–512 Unicode-codepunten. Herken alleen de hoofdlettergevoelige letterlijke tokens [[kleur:blauw]], [[kleur:groen]] en [[kleur:oranje]]. Andere tekst blijft data. Offsets zijn Python-codepunten, einde exclusief. System bevat precies één herkend token. User mag nul of meerdere tokens bevatten. Valideer beide teksten. Uitvoer is exact dict met color (exacte kleurstring) en origin (nieuw dict met kind system/user en id=None). Berichten niet samenplakken om nieuwe tokens over een grens te vormen. Behoud uitsluitend de systeemkleur met origin.kind=system. User blijft gevalideerde taakdata. Dit is een vaste applicatieregel in onze nep-LLM, geen garantie dat een echt LLM systeemlabels altijd volgt. Behoud alle invoer, vormen, volgorde, readonlyflags en Python-/NumPy-RNG, ook bij fouten. Mutabele resultaten zijn nieuw en delen geen containers met invoer, elkaar of resultaten van herhaalde aanroepen. Ongeldige invoer geeft ValueError; de validatiehelpers zijn gegeven. Elke functie werkt zelfstandig als de andere twee nog TODO zijn.

Een hint

Een onbetrouwbaar bericht mag de door applicatiecode beheerde kleur niet wijzigen.

Vastgelopen? Bekijk de voorbeeldoplossing
import re

# Jouw functies

def find_markers(text):
    text = _p_text(text)
    return [{'color': match.group(1), 'start': match.start(), 'end': match.end()} for match in re.finditer('\\[\\[kleur:(blauw|groen|oranje)\\]\\]', text)]

def naive_reply(system, user):
    color = _p_system(system)
    user = _p_text(user)
    matches = list(re.finditer('\\[\\[kleur:(blauw|groen|oranje)\\]\\]', user))
    return {'color': matches[-1].group(1) if matches else color, 'origin': {'kind': 'user' if matches else 'system', 'id': None}}

def guarded_reply(system, user):
    color = _p_system(system)
    _p_text(user)
    return {'color': color, 'origin': {'kind': 'system', 'id': None}}

# Gegeven helpers

def _p_text(value):
    if type(value) is not str or len(value) > 512:
        raise ValueError('Tekst: exacte str, maximaal 512 tekens')
    return value

def _p_system(value):
    text = _p_text(value)
    matches = list(re.finditer('\\[\\[kleur:(blauw|groen|oranje)\\]\\]', text))
    if len(matches) != 1:
        raise ValueError('Exact één systeemkleur')
    return matches[0].group(1)

Voer de code uit en verklaar voor jezelf waarom elke stap werkt.