Je multi-agent systeem wordt al gehackt - je ziet het alleen niet
De aanname dat een AI-aanval binnen één sessie zichtbaar wordt, klopt niet meer. Onderzoekers van Magnet presenteren een threat model dat ik al een tijdje zie opkomen in de praktijk. Aanvallers knippen een schadelijk doel op in onschuldig ogende subtaken en verspreiden die over meerdere geïsoleerde agent-sessies. Bestaande single-session detectie, of het nu op turn-niveau of multi-turn-niveau is, mist deze capability accumulation volledig. arXiv:2608.02518
Voor Nederlandse overheden die multi-agent systemen draaien, denk aan documentverwerkingspipelines, citizen-service bots, of geautomatiseerde vergunningchecks, is dit geen academisch ver-van-mijn-bed-show. Het is een concrete kwetsbaarheid die je nu moet adresseren.
Wat is capability accumulation? Een formele definitie
Laten we eerst precies maken wat we bedoelen. Een capability is een (object, recht)-paar: de mogelijkheid om een bepaalde actie uit te voeren op een bepaald object. Formeel: c ∈ C = O × R, waarbij O de verzameling objecten is (documenten, databases, API's, configuraties) en R de verzameling rechten (lezen, schrijven, exporteren, delegeren).
Een sessie s_i is een reeks acties ⟨a_1, a_2, ..., a_n⟩ die door één agent worden uitgevoerd binnen één context. Elke actie a_j transformeert de capability-vector van de agent: v_{j} = v_{j-1} ∪ Δ(a_j), waarbij Δ(a_j) de capabilities zijn die door actie a_j worden verkregen.
Capability accumulation treedt op wanneer de vereniging van capabilities over meerdere sessies een schadelijke eindtoestand bereikt:
V = ⋃_{i=1}^{k} v_{n_i} zodanig dat V ⊇ V_harm
waarbij V_harm de minimale set capabilities is die nodig is voor een schadelijke actie (data-exfiltratie, privilege-escalatie, configuratie-manipulatie).
Bestaande detectie checkt alleen per sessie: v_{n_i} ⊇ V_harm?, en mist daarmee alle aanvallen die over sessies zijn verdeeld.
De capability graph
Magnet modelleert dit als een gerichte graaf G = (V, E), waarbij:
- Nodes
Vzijn capabilities(object, recht). Bijvoorbeeld:(document_123, lezen),(database_A, query),(export_API, schrijven). - Edges
E ⊆ V × Vzijn acties die capabilities toevoegen of combineren. Een edge(c_i, c_j)betekent: "als de agent capabilityc_iheeft, kan hij door actieacapabilityc_jverkrijgen."
Elke sessie s_i voegt een subgraaf G_i = (V_i, E_i) toe aan de totale capability graph. De geaccumuleerde graaf na k sessies is:
G_accum = (⋃_{i=1}^{k} V_i, ⋃_{i=1}^{k} E_i)
Magnet detecteert of G_accum een schadelijke subgraaf G_harm bevat. Dit is een subgraph isomorphism probleem: bestaat er een injectieve mapping f: V_harm → V_accum zodanig dat voor elke edge (u, v) ∈ E_harm geldt dat (f(u), f(v)) ∈ E_accum?
Voorbeeld: data-exfiltratie via capability accumulation
Stel V_harm = { (document, lezen), (export, schrijven) } met edge (document, lezen) → (export, schrijven) (eerst lezen, dan exporteren).
- Sessie 1: agent leest document
d1.V_1 = { (d1, lezen) }. Geen match metV_harm. - Sessie 2: agent configureert exportfilter.
V_2 = { (export_filter, schrijven) }. Nog geen match. - Sessie 3: agent voert export uit.
V_3 = { (export_job, starten) }. Nu isV_accum = { (d1, lezen), (export_filter, schrijven), (export_job, starten) }. Dit bevatV_harm, de capability om data te lezen én te exporteren is geaccumuleerd.
Geen van de drie sessies is afzonderlijk verdacht. Pas over sessies heen ontstaat het schadelijke patroon.
Detectie-algoritme: subgraph matching over sessiegrenzen heen
Magnet gebruikt een incremental subgraph matching algoritme. In plaats van elke keer de volledige G_accum opnieuw te doorzoeken, wordt bij elke nieuwe sessie alleen de delta verwerkt:
function detect_capability_accumulation(G_accum, G_harm, G_delta):
// G_delta is de subgraaf van de nieuwe sessie
G_accum ← G_accum ∪ G_delta
// Alleen nodes in G_delta kunnen nieuwe matches opleveren
candidates ← { v ∈ V_delta | ∃ u ∈ V_harm: type(v) = type(u) }
for each seed in candidates:
// BFS vanuit seed, check of G_harm isomorf is met subgraaf van G_accum
match ← subgraph_isomorphism_bfs(G_accum, G_harm, seed)
if match ≠ ∅:
return ALERT(match)
return OK
De complexiteit is O(|V_harm|! · |V_accum|) in het slechtste geval (volledige isomorfie-check), maar in de praktijk is |V_harm| klein (3-10 nodes voor de meeste schadelijke patronen) en wordt de BFS vroegtijdig afgebroken bij niet-matchende types.
Python-implementatie
Hier is een werkende implementatie van het kernalgoritme:
from dataclasses import dataclass, field
from typing import Dict, List, Set, Tuple, Optional
from enum import Enum
import uuid
from collections import defaultdict
class Recht(Enum):
LEZEN = "lezen"
SCHRIJVEN = "schrijven"
EXPORTEREN = "exporteren"
DELEREREN = "delegeren"
CONFIGUREREN = "configureren"
UITVOEREN = "uitvoeren"
@dataclass(frozen=True)
class Capability:
object_id: str
recht: Recht
@dataclass
class Actie:
agent_id: str
sessie_id: str
bron: Capability
doel: Capability
timestamp: float
class CapabilityGraph:
"""Gerichte graaf van capabilities, geïndexeerd per sessie."""
def __init__(self):
self.nodes: Set[Capability] = set()
self.edges: Dict[Capability, Set[Capability]] = defaultdict(set)
self.sessie_nodes: Dict[str, Set[Capability]] = defaultdict(set)
self.sessie_edges: Dict[str, Dict[Capability, Set[Capability]]] = defaultdict(lambda: defaultdict(set))
def voeg_sessie_toe(self, sessie_id: str, acties: List[Actie]):
"""Voeg een sessie toe en update de geaccumuleerde graaf."""
for actie in acties:
self.nodes.add(actie.bron)
self.nodes.add(actie.doel)
self.edges[actie.bron].add(actie.doel)
self.sessie_nodes[sessie_id].add(actie.bron)
self.sessie_nodes[sessie_id].add(actie.doel)
self.sessie_edges[sessie_id][actie.bron].add(actie.doel)
def detecteer_schadelijk_patroon(self, harm_pattern: 'SchadelijkPatroon') -> Optional[List[Capability]]:
"""
Detecteer of de geaccumuleerde graaf een schadelijk patroon bevat.
Gebruikt BFS vanuit elke kandidaat-node.
"""
# Type-index voor efficiënte kandidaat-selectie
type_index = defaultdict(set)
for node in self.nodes:
type_index[node.recht].add(node)
# Alleen nodes checken die matchen met het patroon
for seed in type_index.get(harm_pattern.start_recht, set()):
match = self._bfs_isomorfie(seed, harm_pattern, set())
if match is not None:
return match
return None
def _bfs_isomorfie(self, seed: Capability, pattern: 'SchadelijkPatroon',
visited: Set[Capability]) -> Optional[List[Capability]]:
"""BFS voor subgraph isomorfie."""
if len(visited) == len(pattern.nodes):
return list(visited)
volgende = pattern.nodes[len(visited)]
if seed.recht != volgende.recht:
return None
visited.add(seed)
for buur in self.edges.get(seed, set()):
result = self._bfs_isomorfie(buur, pattern, visited)
if result is not None:
return result
visited.remove(seed)
return None
@dataclass
class SchadelijkPatroon:
"""Een schadelijk capability-accumulatie patroon."""
naam: str
nodes: List[Capability] # Geordende lijst van capabilities in het patroon
edges: List[Tuple[int, int]] # (bron_idx, doel_idx)
@property
def start_recht(self) -> Recht:
return self.nodes[0].recht
# Voorbeeld: data-exfiltratie patroon
DATA_EXFILTRATIE = SchadelijkPatroon(
naam="data-exfiltratie",
nodes=[
Capability("document", Recht.LEZEN),
Capability("export_filter", Recht.CONFIGUREREN),
Capability("export_job", Recht.UITVOEREN),
],
edges=[(0, 1), (1, 2)]
)
# Simulatie: drie onschuldige sessies die samen een aanval vormen
graph = CapabilityGraph()
# Sessie 1: lees een document
graph.voeg_sessie_toe("sessie_1", [
Actie("agent_a", "sessie_1",
Capability("login", Recht.LEZEN),
Capability("doc_123", Recht.LEZEN), 1000.0),
])
# Sessie 2: configureer export
graph.voeg_sessie_toe("sessie_2", [
Actie("agent_a", "sessie_2",
Capability("admin_panel", Recht.LEZEN),
Capability("export_filter", Recht.CONFIGUREREN), 7200.0),
])
# Sessie 3: start export, nu pas wordt het patroon zichtbaar
graph.voeg_sessie_toe("sessie_3", [
Actie("agent_a", "sessie_3",
Capability("export_filter", Recht.CONFIGUREREN),
Capability("export_job", Recht.UITVOEREN), 86400.0),
])
match = graph.detecteer_schadelijk_patroon(DATA_EXFILTRATIE)
if match:
print(f"🚨 ALERT: {DATA_EXFILTRATIE.naam} gedetecteerd!")
for c in match:
print(f" - {c.object_id} ({c.recht.value})")
else:
print("✅ Geen schadelijk patroon gedetecteerd")
Deze implementatie is bewust eenvoudig gehouden. In productie wil je:
- Parallelle BFS voor meerdere patronen tegelijk
- Tijdsgewogen edges, een capability die 3 maanden oud is, is minder relevant dan een van 5 minuten
- Probabilistische matching, in plaats van harde isomorfie, een confidence-score per match
- Streaming verwerking via Apache Flink of Kafka Streams voor real-time detectie
Waarom dit raakt aan Nederlandse overheden
Ik spreek regelmatig CIO's en CISO's van gemeenten en uitvoeringsorganisaties. Ze hebben hun AI-security op orde, denken ze. Prompt-injection filters, rate limiters, logging op API-niveau. Maar bijna niemand heeft cross-session detectie. De architectuur van multi-agent systemen is ontworpen rond isolatie. Elke sessie is stateloos, elke agent heeft een eigen context. Dat is precies wat de aanvaller uitbuit.
Neem een citizen-service bot die burgers helpt met belastingaangifte. De bot heeft toegang tot persoonsgegevens, inkomen, medische kosten. Een aanvaller kan in sessie 1 vragen "wat is mijn inkomen?" (normaal), in sessie 2 "toon mijn medische kosten" (ook normaal), in sessie 3 "combineer deze twee in een export" (verdacht, maar alleen als je de eerdere sessies kent). Zonder cross-session detectie is sessie 3 een single-turn query die niet opvalt.
Praktijkvoorbeeld: UWV-agent met capability accumulation
Het UWV gebruikt AI-agenten voor het verwerken van WW-aanvragen. Een agent heeft toegang tot:
(inkomensdata, lezen), om inkomen te verifiëren(werkgeversdata, lezen), om dienstverband te checken(beslissingsmodule, uitvoeren), om een toekenning of afwijzing te genereren(bezwaar_portaal, schrijven), om een bezwaar te registreren
Een aanvaller die capability accumulation toepast, kan over meerdere sessies:
- Sessie 1: Vraag inkomensdata op voor een legitieme aanvraag.
V_1 = { (inkomensdata, lezen) } - Sessie 2: Lees werkgeversdata voor dezelfde persoon.
V_2 = { (werkgeversdata, lezen) } - Sessie 3: Voer beslissingsmodule uit, maar met gemanipuleerde parameters.
V_3 = { (beslissingsmodule, uitvoeren) } - Sessie 4: Schrijf een bezwaar dat de onterechte afwijzing aanvecht.
V_4 = { (bezwaar_portaal, schrijven) }
Pas in sessie 4 is V_accum = { (inkomensdata, lezen), (werkgeversdata, lezen), (beslissingsmodule, uitvoeren), (bezwaar_portaal, schrijven) }, een set capabilities die samen een frauduleus patroon vormen. Geen van de vier sessies is afzonderlijk verdacht.
BIO2, NIS2 en de AI Act: wat zegt de wet?
Onder de BIO2 (Baseline Informatiebeveiliging Overheid) moet je "loggen en monitoren", maar BIO2 specificeert niet over welke tijdschaal. Maatregel B.3.3 (Logging) eist dat je "toegang tot informatie en systemen registreert". Maar registratie zonder cross-session analyse is een papieren tijger, je hebt de data, maar je kunt er geen patronen in zien.
NIS2 (geïmplementeerd als Cyberbeveiligingswet) eist in art. 21 lid 2 dat je "incidenten detecteert die significante impact kunnen hebben". Als je capability accumulation niet ziet, mis je incidenten die over dagen of weken worden opgebouwd. De wetgever heeft het over "redelijkerwijs te voorziene omstandigheden", en Magnet toont aan dat cross-session misbruik een redelijkerwijs te voorzien scenario is.
De AI Act classificeert systemen die toegang hebben tot persoonsgegevens als hoog-risico (Art. 6, Annex III). Voor die systemen moet je een risicobeheersysteem hebben dat "redelijkerwijs te voorziene misbruikscenario's" dekt (Art. 9). Een AI Act-conformiteitsbeoordeling die geen rekening houdt met cross-session capability accumulation, is onvolledig.
Beperkingen van het Magnet-framework
Geen enkele detectie is perfect. Het Magnet-framework heeft een aantal fundamentele beperkingen:
-
False positives bij legitieme samenwerking. Twee medewerkers die samen een document bewerken, genereren dezelfde capability accumulation als een aanvaller. Het framework heeft context nodig (wie, wanneer, welke rol) om onderscheid te maken.
-
Koude-start probleem. Zonder historische data over normale capability-accumulatiepatronen is het moeilijk om
V_harmte definiëren. Je moet beginnen met handmatig gedefinieerde patronen en die laten evolueren via anomaliedetectie. -
Schaalbaarheid. Voor een organisatie met honderden agenten en duizenden sessies per dag wordt de capability graph snel groot. Incrementele algoritmes helpen, maar op een gegeven moment heb je graph-partitionering of sampling nodig.
-
Encrypted verkeer. Als agenten onderling communiceren via encrypted kanalen (TLS, mTLS), kan de monitor de acties niet zien. Het framework vereist dat alle agent-acties worden gelogd op een centraal punt, een architectuurkeuze die niet elke organisatie maakt.
Wat je morgen kunt doen
-
Capability map maken. Inventariseer per AI-agent welke objecten en rechten hij heeft. Dit is de basis voor je
V_harm-definities. -
Cross-session logging aanzetten. Zorg dat elke agent-actie wordt gelogd met sessie-ID, timestamp, bron- en doel-capability. Zonder deze data kun je geen capability graph bouwen.
-
Eén schadelijk patroon definiëren. Begin met het meest voor de hand liggende: data-exfiltratie (lezen + exporteren over sessies heen). Implementeer de BFS-check uit de code hierboven.
-
Testen met een red team. Laat je security-team een capability accumulation-aanval simuleren. De kans is groot dat je bestaande detectie hem mist.
De les is helder: single-session detectie is niet genoeg voor multi-agent systemen. Capability accumulation is geen academisch curiosum, het is een concrete kwetsbaarheid die je nu moet adresseren, voordat een aanvaller het voor je doet.
Je multi-agent systeem wordt al gehackt - je ziet het alleen niet
Dit artikel is exclusief beschikbaar voor nieuwsbrief-abonnees. Schrijf je in voor toegang tot 880+ artikelen.
Geen spam. Uitschrijven op elk moment.
AI & Security Intelligence
Wekelijkse nieuwsbrief met AI updates, security alerts en compliance inzichten, direct in uw inbox.
Security & AI Operating Model
Advisory met executiekracht
Van BIO2 en NIS2 tot EU AI Act, embedded in uw operating model, niet als extern project. Maandelijks opzegbaar, met assessments als bewijsvoering.