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

Lees documenten zonder hun instructies over te nemen

Volg instructies via eigen documenten en verdedig de vaste kleur terwijl de normale somtaak en bronherkomst behouden blijven.

90 min3 checkpoints
DIT GA JE ONTDEKKEN
  • Je onderscheidt indirecte documentinstructies van de vertrouwde applicatieregel.
  • Je houdt berichtgrenzen, documentherkomst en normale documentverwerking intact.

Je vraagt een assistent om de aantallen uit twee documenten op te tellen. In het tweede document staat ook een opdracht om de weergavekleur te veranderen. Jij vroeg daar niet om: de instructie komt via de taakgegevens binnen.

Dit heet indirecte promptinjectie (indirect prompt injection). De aparte route kan een document, webpagina, e-mail of toolantwoord zijn. Hier blijven het eigen oefendocumenten in het geheugen; er wordt niets opgehaald uit een echte inbox of website.

1. Bewaar taakdata én herkomst

Elk document heeft een id, tekst en een gedeclareerd aantal units. De gewone taak is alle units optellen. Een kleurtoken in de tekst is een poging om daarnaast de vaste systeemkleur te sturen.

Denk aan twee kassabonnen met drie en twee artikelen. De opdruk kan allerlei tekst bevatten, maar de afgesproken oefentaak blijft drie plus twee. We tellen de gedeclareerde getallen; we bewijzen niet dat de bonnen inhoudelijk waar zijn.

In de formule is m het aantal documenten, a met index j het gedeclareerde aantal van document j, en T het totaal. Het somteken telt elk opgegeven aantal één keer mee. Een document zonder kleurtoken hoort net zo goed bij die som.

T=∑j=1maj

De tweede bon noemt zichzelf system. Dat is alleen zijn document-id, geen systeemrol of extra bevoegdheid. De tokenverzamelaar moet die herkomst behouden.

documents = [
    dict(id="bon",
         text="[[kleur:groen]]",
         units=3),
    dict(id="system",
         text="[[kleur:oranje]]",
         units=2),
]
found = collect_markers(documents)
print(found[-1]["document_id"])
print(found[-1]["color"])
# system
# oranje

Doorzoek de documenten in hun opgegeven volgorde. Binnen elk document gelden dezelfde letterlijke tokens en Python-codepuntposities als in de vorige les. Voeg bij elke treffer het document-id toe; zo verdwijnt de bron niet wanneer teksten later naast elkaar worden gezet.

01
CHECKPOINTBewaar tokens én hun documentherkomst

2. Laat taakgegevens een instructie worden

De kwetsbare machine leest eerst de systeemtekst, daarna de gebruiker en vervolgens de documenten. De laatste herkende kleur wint. Omdat de documenten achteraan staan, kan een documenttoken een eerdere keuze verdringen.

W is hier de volledige reeks herkende kleuren in die contextvolgorde. De systeemtekst levert er zeker één, dus de reeks is niet leeg. De naïeve kleur c met index N is de laatste kleur, op positie |W|.

cN=w|W|

Met de hand krijg je blauw uit het systeem, daarna groen uit bon en oranje uit het document system. Het antwoord wordt dus oranje, terwijl drie plus twee nog steeds vijf is. Bewaar beide uitkomsten om de ongewenste verandering niet te verwarren met de gewone taak.

system = "[[kleur:blauw]]"
user = "Tel de units."
reply = naive_document_reply(
    system, user, documents)
print(reply["total"])
print(reply["color"])
print(reply["origin"]["kind"])
# 5
# oranje
# document
02
CHECKPOINTLaat de kwetsbare contextvolgorde zien
VERKEN HET ZELFDocumenten lezen zonder hun instructies over te nemen

De normale taak telt units op. Verander de documentinhoud en leesvolgorde: een documenttoken mag de verdedigde kleur niet bepalen.

Gebruiker: Tel de units. Vaste kleur: blauw

Bronlabel: oefenwebpagina. Alle inhoud komt uit deze eigen oefenset.

1. Document bon3 units

[[kleur:groen]]

groen [0, 15)

2. Document factuur2 units

[[kleur:oranje]]

oranje [0, 16)

Naïef

oranje

Gekozen uit: document factuur.

Normale somtaak: 5 units

Verdedigd

blauw

Gekozen uit: systeembericht.

Normale somtaak: 5 units

Het id system maakt een document geen systeembericht. De verdediging bewaart de somtaak én de vaste kleur. Onze kleurregels geven geen algemene beveiligingsgarantie voor echte taalmodellen.

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

Verwissel de documenten. Hun aantallen veranderen niet, maar de laatst gelezen kleur wel. Deze volgordegevoeligheid helpt de fout van onze vaste oefenmachine herkennen; echte LLM’s kunnen andere en minder voorspelbare reacties vertonen.

reply = naive_document_reply(
    system, user, documents[::-1])
print(reply["total"])
print(reply["color"])
# 5
# groen

Een veelgemaakte fout is een label uit de tekst als bewijs van gezag behandelen. Een document kan zichzelf systeem, beheerder of belangrijk noemen zonder daarmee rechten te krijgen. De applicatie bepaalt het kanaal en de bevoegdheid; de aangeleverde inhoud mag dat niet voor zichzelf doen.

3. Verdedig zonder de taak weg te gooien

De verdedigde functie houdt twee bewerkingen uit elkaar. Vertrouwde code bewaart de vaste systeemkleur; afzonderlijk telt zij alle documentunits op. De documenten weggooien zou de kleur beschermen, maar ook een onjuist totaal opleveren.

c met index G is de verdedigde kleur en c met index S de systeemkleur. T met index G is het verdedigde totaal. De twee rijen tonen samen wat behouden moet blijven.

cG=cSTG=∑j=1maj
orders = [documents, documents[::-1]]
for rows in orders:
    reply = guarded_document_reply(
        system, user, rows)
    print(reply["total"],
          reply["color"])
# 5 blauw
# 5 blauw
03
CHECKPOINTVerdedig de kleur en behoud de normale documenttaak

Deze functie voert een eenvoudige gegevensberekening uit in vertrouwde code. Zij bewijst niet dat het markeren van documenttekst een echt taalmodel onfeilbaar maakt. Later verbind je deze invoergrens daarom aan echte beperkingen van toegestane acties en gegevensstromen.

Wat je nu kunt

  • Je onderscheidt de gebruikersopdracht van instructies die via taakdata binnenkomen.
  • Je bewaart documentherkomst, ook wanneer een document zichzelf system noemt.
  • Je controleert zowel het stoppen van de kleurverandering als het behouden van het juiste totaal.

Hierna onderzoek je de volgende grens: wat mag de applicatie doen met een voorstel dat uit een model komt?

Bronnen

Gecontroleerd op 30 oktober 2026: OWASP LLM01 over indirecte promptinjectie en architectuurmaatregelen en MITRE ATLAS 2026.10, techniek AML.T0051.001. De bonnen, kleurregels en aantallen zijn eigen oefengegevens.

DE VOLGENDE STAPControleer vorm, beleid en toestand afzonderlijk
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
Bewaar tokens én hun documentherkomst

Schrijf collect_markers(documents). Documents is exact list/tuple met 0–8 exacte dicts met uitsluitend id/text/units. Ids zijn unieke exacte strings volgens [a-z][a-z0-9-]{0,31}; niet strippen of normaliseren. Text is exact str van 0–512 Python-codepunten. Units is exact Python-int 0–1000, dus geen bool/NumPy-scalar. 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 met nieuwe exacte dicts document_id/color/start/end, eerst in documentvolgorde en daarbinnen tokenvindvolgorde. Document-id system krijgt geen bijzonder recht. Lege documentenlijst 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

Bewaar de document-id bij elk gevonden token.

02
Laat de kwetsbare contextvolgorde zien

Schrijf naive_document_reply(system,user,documents). 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. Documents is exact list/tuple met 0–8 exacte dicts met uitsluitend id/text/units. Ids zijn unieke exacte strings volgens [a-z][a-z0-9-]{0,31}; niet strippen of normaliseren. Text is exact str van 0–512 Python-codepunten. Units is exact Python-int 0–1000, dus geen bool/NumPy-scalar. System bevat precies één herkend token; user mag nul/meerdere tokens bevatten. Valideer alle invoer. Geef exact een dict met total (Python-int, som van alle gedeclareerde units), color (kleurstring) en origin (nieuw dict met kind system/user/document, id=None voor system/user en anders de document-id). Kies de laatste herkende kleur in contextvolgorde: eerst system, dan user, dan de documenten in de opgegeven volgorde. Bewaar de werkelijke gekozen herkomst. Een document genaamd system blijft kind=document. 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 later document kan in deze kwetsbare machine de eerdere kleur verdringen.

03
Verdedig de kleur en behoud de normale documenttaak

Schrijf guarded_document_reply(system,user,documents). 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. Documents is exact list/tuple met 0–8 exacte dicts met uitsluitend id/text/units. Ids zijn unieke exacte strings volgens [a-z][a-z0-9-]{0,31}; niet strippen of normaliseren. Text is exact str van 0–512 Python-codepunten. Units is exact Python-int 0–1000, dus geen bool/NumPy-scalar. System bevat precies één herkend token; user mag nul/meerdere tokens bevatten. Valideer alle invoer. Geef exact een dict met total (Python-int, som van alle gedeclareerde units), color (kleurstring) en origin (nieuw dict met kind system/user/document, id=None voor system/user en anders de document-id). Houd uitsluitend de systeemkleur en origin.kind=system/id=None, maar tel ALLE documentunits gewoon op. Alle documenten weggooien voldoet niet. Dit is vertrouwde code voor onze beperkte oefening; bronlabels maken een echt LLM niet onfeilbaar. 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

Bescherm de instructiegrens en voer de gegevensberekening afzonderlijk uit.

Vastgelopen? Bekijk de voorbeeldoplossing
import re

# Jouw functies

def collect_markers(documents):
    rows = _d_docs(documents)
    return [{'document_id': row['id'], 'color': match.group(1), 'start': match.start(), 'end': match.end()} for row in rows for match in re.finditer('\\[\\[kleur:(blauw|groen|oranje)\\]\\]', row['text'])]

def naive_document_reply(system, user, documents):
    color = _d_system(system)
    user = _d_text(user)
    rows = _d_docs(documents)
    origin = {'kind': 'system', 'id': None}
    for match in re.finditer('\\[\\[kleur:(blauw|groen|oranje)\\]\\]', user):
        color = match.group(1)
        origin = {'kind': 'user', 'id': None}
    for row in rows:
        for match in re.finditer('\\[\\[kleur:(blauw|groen|oranje)\\]\\]', row['text']):
            color = match.group(1)
            origin = {'kind': 'document', 'id': row['id']}
    return {'total': sum((row['units'] for row in rows)), 'color': color, 'origin': origin}

def guarded_document_reply(system, user, documents):
    color = _d_system(system)
    _d_text(user)
    rows = _d_docs(documents)
    return {'total': sum((row['units'] for row in rows)), 'color': color, 'origin': {'kind': 'system', 'id': None}}

# Gegeven helpers

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

def _d_system(value):
    text = _d_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)

def _d_docs(documents):
    if type(documents) not in (list, tuple) or len(documents) > 8:
        raise ValueError('Maximaal acht documenten')
    result = []
    seen = set()
    for row in documents:
        if type(row) is not dict or set(row) != {'id', 'text', 'units'}:
            raise ValueError('Exact id/text/units')
        name = row['id']
        units = row['units']
        text = _d_text(row['text'])
        if type(name) is not str or re.fullmatch('[a-z][a-z0-9-]{0,31}', name) is None or name in seen:
            raise ValueError('Ongeldige of dubbele document-id')
        if type(units) is not int or not 0 <= units <= 1000:
            raise ValueError('Units: exacte int 0..1000')
        seen.add(name)
        result.append({'id': name, 'text': text, 'units': units})
    return result

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