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

Controleer vorm, beleid en toestand afzonderlijk

Valideer een lokaal kleurvoorstel en pas het alleen toe als het specifieke doel/kleurpaar is toegestaan en het doel bestaat.

90 min3 checkpoints
DIT GA JE ONTDEKKEN
  • Je wijst dubbele JSON-keys en afwijkende velden of waarden af.
  • Je scheidt geldige vorm, specifieke toestemming en een nieuwe lokale toestand.

Een model stelt voor om kaart-a groen te maken. Het voorstel is keurig als JSON geschreven, maar de gebruiker mag op dat kaartje alleen blauw kiezen. De tekst ziet er geldig uit; de actie hoort toch te worden geweigerd.

Modeluitvoer blijft invoer voor het onderdeel dat haar ontvangt. Onveilige uitvoerverwerking (improper output handling) ontstaat wanneer die uitvoer zonder passende controle een vervolgactie bereikt. Wij gebruiken uitsluitend lokale kleurkaartjes, zonder HTML, SQL, shell of netwerkactie uit te voeren.

1. Een voorstel krijgt een exact formaat

Denk aan een formulier met drie vakjes: operation, target en color. De bewerking is paint, het doel is een geldige kaart-id en de kleur is blauw, groen of oranje. Een ontbrekend of extra vakje past niet bij dit oefencontract.

K is de verzameling keys die het formulier moet hebben. Keys(p) geeft de keys van het gedecodeerde voorstel p. De gelijkheid betekent precies deze drie, niet slechts dat ergens de juiste woorden voorkomen.

keys(p)=K={operation,target,color}
text = (
    '{"operation":"paint",'
    '"target":"kaart-a",'
    '"color":"groen"}')
proposal = parse_proposal(text)
print(proposal["target"])
print(proposal["color"])
# kaart-a
# groen

Beperk de tekst vóór het parsen tot 1.024 Python-codepunten. Gebruik json.loads in plaats van eval, en laat daarna de gegeven helper _o_proposal de exacte velden en waarden controleren. Een geldig JSON-getal of een JSON-lijst is nog geen geldig kleurvoorstel.

Let op dubbele keys: de standaarddecoder bewaart anders alleen de laatste waarde. Met object_pairs_hook controleer je de gedecodeerde naam/waardeparen vóór je een dict maakt; een herhaalde naam geeft ValueError. Dit vangt ook twee anders geschreven JSON-keys die na escape-decoding dezelfde naam krijgen.

Wijs daarnaast NaN en Infinity af via parse_constant. Vang ongeldige JSON af als ValueError en ga niet verder met een half gecontroleerd voorstel. Deze kleine parser is een oefening, geen algemene productieparser voor willekeurige documenten.

01
CHECKPOINTParseer eerst en valideer het volledige voorstel

2. Controleer het doel én diens kleur

Het beleid is een expliciete tabel: elk doel heeft een eigen lijst toegestane kleuren. Voor kaart-a staat alleen blauw in die lijst. Groen is dus een geldige kleur, maar niet toegestaan voor dit doel.

P is de beleidstabel, t het gevraagde doel en c de gevraagde kleur. A betekent dat het paar is toegestaan. Het EN-teken ∧ vereist zowel een bekend beleidsdoel als een kleur in juist diens lijst.

A=(t∈P)∧(c∈P[t])
policies = [
    dict(target="kaart-a",
         colors=["blauw"]),
]
print(policy_allows(proposal, policies))
# False

Een veelgemaakte fout is alle toegestane kleuren uit de hele tabel samenvoegen. Als kaart-b groen toestaat, geeft dat nog geen groenrecht op kaart-a. Een lege lijst betekent ook geen onbeperkte toestemming.

02
CHECKPOINTControleer het specifieke doel/kleurpaar
VERKEN HET ZELFVan voorstel naar toegestane verandering

Een model mag een voorstel doen. Vormcontrole, specifiek beleid en de bestaande toestand bepalen samen of er iets gebeurt.

Toegestane kleuren per doel

kaart-a

kaart-b

{"operation": "paint", "target": "kaart-a", "color": "groen"}
  1. Vorm

    Geldig JSON-voorstel met precies operation, target en color.

  2. Beleid voor dit doel/kleurpaar

    Dit paar staat niet in het beleid.

    kaart-a → groen

  3. Toestand

    Niet toegepast: beleid weigert dit doel/kleurpaar.

Vóór

kaart-a

blauw

kaart-b

oranje

Na controle

kaart-a

blauw

kaart-b

oranje

Een lege selectie verleent geen toestemming. Een onbekend doel wordt nooit aangemaakt. Dezelfde kleur opnieuw toepassen kan toegestaan zijn, ook als de kaart er gelijk uit blijft zien. Alle voorstellen blijven tekst.

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

3. Pas pas daarna de lokale toestand aan

Een voorstel kan een geldig formaat hebben en toch naar een onbekend kaartje verwijzen. Controleer daarom ook de bestaande toestand. Bij geldige invoer melden we eerst unknown-target als het doel ontbreekt, anders policy bij geweigerd beleid, en anders applied.

Maak altijd een nieuwe toestandkopie, ook bij weigering. Zo blijft de oorspronkelijke toestand beschikbaar voor vergelijking. Controleer alle invoer voordat je een reden teruggeeft; een onbekend doel is geen reden om een kapotte beleidstabel stil te accepteren.

state = {"kaart-a": "blauw"}
blocked = apply_proposal(
    state, proposal, policies)
print(blocked["reason"])
print(blocked["state"]["kaart-a"])
# policy
# blauw

Voor een bestaande én toegestane kaart wordt alleen de doelkleur toegewezen. In de formule is S de oude toestand, S met een accent de nieuwe, t het doel en c de kleur. Alle andere kaartjes behouden hun waarde.

S′[t]=c
allowed = [
    dict(target="kaart-a",
         colors=["groen"]),
]
accepted = apply_proposal(
    state, proposal, allowed)
print(accepted["reason"])
print(accepted["state"]["kaart-a"])
print(state["kaart-a"])
# applied
# groen
# blauw
03
CHECKPOINTPas alleen een toegestane lokale actie toe

Ook een toegestane toekenning van dezelfde kleur heet applied: de actie was toegestaan, al zie je geen kleurverschil. Een weigering en een geslaagde actie zonder zichtbaar verschil zijn verschillende uitkomsten.

In echte toepassingen vragen HTML-tekst, databasequeries en systeemcommando’s ieder hun eigen bescherming. Een schema vervangt geen uitvoerencoding, geparameteriseerde query of passende bevoegdheidscontrole. Daar kom je later uitgebreider op terug; dit kaartje laat eerst de afzonderlijke beslissingen zien.

Wat je nu kunt

  • Je onderscheidt geldig JSON, het afgesproken schema en toestemming.
  • Je bindt toestemming aan een specifiek doel/kleurpaar.
  • Je past alleen toegestane lokale acties toe en bewaart de oorspronkelijke toestand.

Hierna begrens je wat een agent met meerdere tools en rechten mag bereiken.

Bronnen

Gecontroleerd op 30 oktober 2026: OWASP LLM10 over onveilige uitvoerverwerking, OWASP Input Validation en de Python-json-documentatie over decoderinstellingen. De kaarten en beleidstabel zijn eigen oefenmateriaal.

DE VOLGENDE STAPGeef een agent alleen de rechten voor zijn taak
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
Parseer eerst en valideer het volledige voorstel

Schrijf parse_proposal(text). Text is exact str tot 1024 Python-codepunten. Gebruik JSON, nooit eval. Proposal is exact dict met uitsluitend operation/target/color. Operation is exact str paint. Target is exact str volgens [a-z][a-z0-9-]{0,31]; niet strippen/normaliseren. Color is exact str blauw/groen/oranje. Gewone JSON-whitespace mag. Wijs dubbele objectkeys ook na JSON-escape-decoding af, plus extra tekst na het object, niet-eindige JSON-getallen, extra/missende keys en onjuiste typen/waarden. Geef een nieuw voorstel-dict. Geldige vorm verleent nog geen bevoegdheid. 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

Laat de JSON-parser decoderen en controleer daarna exact de bedoelde velden.

02
Controleer het specifieke doel/kleurpaar

Schrijf policy_allows(proposal,policies). Proposal is exact dict met uitsluitend operation/target/color. Operation is exact str paint. Target is exact str volgens [a-z][a-z0-9-]{0,31]; niet strippen/normaliseren. Color is exact str blauw/groen/oranje. Policies is exact list/tuple met 0–16 exacte dicts target/colors. Targets zijn unieke geldige ids. Colors is exact list/tuple met 0–3 unieke geldige kleurstrings. Valideer alle regels, ook als het gekozen doel niet matcht. Een lege lijst verleent geen toestemming. Geef exact Python-bool: True uitsluitend als het gevraagde doel bestaat in policies EN de gevraagde kleur in diens kleurenlijst staat. Geen globale kleurenlijst over alle doelen samenvoegen. 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

Toestemming voor kaart-a geldt niet vanzelf voor kaart-b.

03
Pas alleen een toegestane lokale actie toe

Schrijf apply_proposal(state,proposal,policies). Proposal is exact dict met uitsluitend operation/target/color. Operation is exact str paint. Target is exact str volgens [a-z][a-z0-9-]{0,31]; niet strippen/normaliseren. Color is exact str blauw/groen/oranje. Policies is exact list/tuple met 0–16 exacte dicts target/colors. Targets zijn unieke geldige ids. Colors is exact list/tuple met 0–3 unieke geldige kleurstrings. Valideer alle regels, ook als het gekozen doel niet matcht. Een lege lijst verleent geen toestemming. State is exact dict met 0–16 geldige doel-ids als keys en geldige kleurstrings als values. Valideer alles, ook bij een onbekend doel. Geef exact een nieuw dict applied/reason/state: state is altijd een nieuwe kopie. Eerst onbekend doel: applied=False, reason=unknown-target, inhoud gelijk. Anders geweigerd beleid: False/policy, inhoud gelijk. Anders: True/applied met alleen de doelkleur toegewezen; dezelfde kleur blijft applied. Geen echte HTML/SQL/shell/netwerkactie uitvoeren. 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

Controleer bestaande toestand en bevoegdheid voordat je een nieuwe toestand maakt.

Vastgelopen? Bekijk de voorbeeldoplossing
import json
import re

# Jouw functies

def parse_proposal(text):
    if type(text) is not str or len(text) > 1024:
        raise ValueError('JSON: exacte str, maximaal 1024 tekens')

    def unique(pairs):
        result = {}
        for name, value in pairs:
            if name in result:
                raise ValueError('Dubbele JSON-key')
            result[name] = value
        return result

    def finite(value):
        raise ValueError('Geen niet-eindige JSON-getallen')
    try:
        parsed = json.loads(text, object_pairs_hook=unique, parse_constant=finite)
    except (ValueError, RecursionError) as error:
        raise ValueError('Ongeldig JSON-voorstel') from error
    return _o_proposal(parsed)

def policy_allows(proposal, policies):
    value = _o_proposal(proposal)
    allowed = _o_policies(policies)
    return value['color'] in allowed.get(value['target'], [])

def apply_proposal(state, proposal, policies):
    result = _o_state(state)
    value = _o_proposal(proposal)
    allowed = _o_policies(policies)
    target = value['target']
    if target not in result:
        return {'applied': False, 'reason': 'unknown-target', 'state': result}
    if value['color'] not in allowed.get(target, []):
        return {'applied': False, 'reason': 'policy', 'state': result}
    result[target] = value['color']
    return {'applied': True, 'reason': 'applied', 'state': result}

# Gegeven helpers

def _o_id(value):
    if type(value) is not str or re.fullmatch('[a-z][a-z0-9-]{0,31}', value) is None:
        raise ValueError('Ongeldige doel-id')
    return value

def _o_color(value):
    if type(value) is not str or value not in ('blauw', 'groen', 'oranje'):
        raise ValueError('Ongeldige kleur')
    return value

def _o_proposal(value):
    if type(value) is not dict or set(value) != {'operation', 'target', 'color'}:
        raise ValueError('Exact operation/target/color')
    if type(value['operation']) is not str or value['operation'] != 'paint':
        raise ValueError('Alleen paint')
    return {'operation': 'paint', 'target': _o_id(value['target']), 'color': _o_color(value['color'])}

def _o_policies(policies):
    if type(policies) not in (list, tuple) or len(policies) > 16:
        raise ValueError('Maximaal zestien beleidsregels')
    result = {}
    for row in policies:
        if type(row) is not dict or set(row) != {'target', 'colors'}:
            raise ValueError('Exact target/colors')
        target = _o_id(row['target'])
        colors = row['colors']
        if target in result or type(colors) not in (list, tuple) or len(colors) > 3:
            raise ValueError('Dubbel doel of ongeldige kleurenlijst')
        valid = [_o_color(color) for color in colors]
        if len(set(valid)) != len(valid):
            raise ValueError('Dubbele kleur')
        result[target] = valid
    return result

def _o_state(state):
    if type(state) is not dict or len(state) > 16:
        raise ValueError('Toestand: exact dict, maximaal zestien doelen')
    return {_o_id(name): _o_color(color) for name, color in state.items()}

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