Zum Inhalt springen
Projekt besprechen

30. September 2026

Wie beweist man, dass etwas Gefährliches nicht passiert?

Security EngineeringAI SafetyTestingComplianceSoftwarequalität

Sicherheit zeigt sich meist durch das, was nicht geschieht

Viele Anforderungen klingen zunächst einfach:

  • Unberechtigte Nutzer dürfen keine Rechnungen lesen.
  • Ein Modell darf keine externen Systeme ohne Freigabe ansteuern.
  • Personenbezogene Daten dürfen nicht in Logs oder Prompts landen.
  • Ein Deployment darf nicht ohne Vier-Augen-Freigabe in Produktion gehen.
  • Ein Kunde darf nur seine eigenen Mandantendaten sehen.

Das sind keine Anforderungen an einen sichtbaren Erfolgsfall. Sie beschreiben die Abwesenheit eines unerwünschten Verhaltens.

Genau darin liegt die Schwierigkeit: Ein funktionierender Happy Path ist kein Nachweis für Sicherheit. Wenn sich ein berechtigter Nutzer anmeldet und seine Rechnung sieht, wissen wir nur, dass der erlaubte Pfad funktioniert. Wir wissen nicht, ob eine manipulierte invoiceId, ein abgelaufenes Token, ein anderer Tenant oder eine direkte API-Anfrage den Schutz umgeht.

Sicherheit lässt sich selten vollständig beweisen. Aber man kann belastbare Evidenz erzeugen: für klar benannte verbotene Pfade, für die Wirksamkeit von Kontrollen und für die Reaktion des Systems, wenn Annahmen verletzt werden.

Der Denkfehler: „Der Test ist grün, also ist das System sicher“

Ein Test wie dieser ist sinnvoll, aber unzureichend:

it("zeigt einem angemeldeten Kunden seine Rechnung", async () => {
  const response = await api.get("/invoices/inv-123", {
    headers: authHeader(customerA),
  });
 
  expect(response.status).toBe(200);
  expect(response.body.customerId).toBe(customerA.id);
});

Er prüft einen erlaubten Zugriff. Für eine Mandantentrennung fehlt aber die zentrale Aussage:

Kunde A kann niemals eine Rechnung von Kunde B lesen.

Diese Aussage wird nicht dadurch wahr, dass Kunde A seine eigene Rechnung lesen kann. Sie braucht mindestens einen Gegenversuch:

it("verweigert den Zugriff auf Rechnungen anderer Kunden", async () => {
  const response = await api.get("/invoices/inv-of-customer-b", {
    headers: authHeader(customerA),
  });
 
  expect(response.status).toBe(404);
  expect(response.body).not.toHaveProperty("amount");
  expect(response.body).not.toHaveProperty("customerId");
});

Ob hier 403 oder 404 richtig ist, hängt vom Bedrohungsmodell ab. Ein 404 kann verhindern, dass IDs als gültig bestätigt werden. Wichtig ist etwas anderes: Der verbotene Pfad wird ausdrücklich getestet, und die erwartete sichere Reaktion ist definiert.

In Projekten sehe ich häufig Test-Suiten mit vielen Erfolgsfällen und wenigen absichtlichen Fehlversuchen. Das ist für fachliche Korrektheit oft ausreichend. Für Security Engineering, AI Safety und Compliance ist es meist zu wenig.

Von einer Negativanforderung zu einer prüfbaren Kontrolle

Die Formulierung „Das darf nicht passieren“ ist ein guter Anfang, aber noch keine testbare Anforderung. Sie muss präzisiert werden.

Eine brauchbare Zerlegung besteht aus fünf Fragen:

  1. Was ist das geschützte Gut?
    Beispielsweise Kundendaten, Produktionszugriff, Geldbewegungen, medizinische Empfehlungen oder ein externes Tool.

  2. Wer oder was darf darauf zugreifen?
    Ein Benutzer mit einer bestimmten Rolle, ein Service-Account, ein Workflow nach Freigabe oder niemand.

  3. Welche verbotenen Pfade gibt es?
    Manipulierte Objekt-ID, fehlender Tenant-Filter, Prompt Injection, Replay eines Tokens, Umgehung über Batch-API oder direkten Datenbankzugriff.

  4. Welche Kontrolle soll den Pfad stoppen?
    Serverseitige Autorisierung, Policy Engine, Schema-Validierung, Allowlist, Approval-Gate, Netzwerkregel oder Datenbank-Constraint.

  5. Welche beobachtbare Reaktion erwarten wir?
    Anfrage wird abgewiesen, Tool-Aufruf wird nicht ausgeführt, keine Daten verlassen die Grenze, ein Audit-Ereignis wird geschrieben, ein Alarm wird ausgelöst.

Damit wird aus einer vagen Aussage eine Sicherheitsinvariante:

Für jede Anfrage gilt: Die zurückgegebenen Rechnungen gehören zum Tenant aus dem verifizierten Token, nicht zu einem vom Client gelieferten Tenant-Parameter.

Oder im AI-Kontext:

Ein Modell darf nur Tools aus der für den Workflow definierten Allowlist aufrufen; Tool-Argumente müssen gegen ein Schema validiert werden; bei fehlender Freigabe findet kein Seiteneffekt statt.

Solche Sätze sind keine Formalverifikation. Sie sind aber präzise genug, um Architektur, Implementierung, Tests und Audit-Nachweise an derselben Aussage auszurichten.

Verbotene Pfade zuerst modellieren

Teams beginnen häufig mit User Stories: „Als Sachbearbeiter möchte ich …“ Für Sicherheitsarbeit ist eine zweite Perspektive nötig: „Wie könnte diese Story falsch ausgeführt werden?“

Für einen Dokumentenexport könnte eine kleine Tabelle genügen:

Erlaubter VorgangVerbotener PfadErwartete KontrolleNachweis
Nutzer exportiert eigene DatenNutzer setzt fremde tenantIdTenant wird aus dem Token abgeleitetAPI-Integrationstest
Admin startet ExportStandardnutzer ruft Admin-Endpunkt aufServerseitiger RollencheckNegativtest mit Standardrolle
Export wird bereitgestelltSignierter Download-Link wird geteiltKurzlebiger, gebundener TokenTest nach Ablauf und mit anderem Nutzer
Export wird protokolliertExport ohne Audit-EreignisAudit-Write ist Teil der Transaktion bzw. ein überwachten Outbox-FlowIntegrations- und Monitoringtest

Die Tabelle zwingt zu Entscheidungen, die sonst implizit bleiben. Insbesondere macht sie sichtbar, wenn eine Kontrolle nur im Frontend existiert. Ein ausgeblendeter Button ist keine Zugriffskontrolle. Der relevante Test muss den Backend-Endpunkt ohne UI aufrufen.

Negative Tests sind kein Sonderfall

Negative Tests prüfen, dass das System bei ungültigen, unerlaubten oder gefährlichen Eingaben sicher reagiert. Sie gehören nicht ans Ende eines Projekts und nicht ausschließlich in einen Penetrationstestbericht.

Typische Kategorien sind:

  • Autorisierung: falsche Rolle, fremder Tenant, fehlende Berechtigung, indirekter Zugriff über verwandte Ressourcen.
  • Authentisierung und Sitzungen: abgelaufenes Token, widerrufenes Token, falsche Audience, Replay, fehlende MFA bei sensiblen Aktionen.
  • Eingabegrenzen: ungültige Formate, zu große Payloads, unerwartete Felder, Pfadtraversal, SQL- oder Template-Injection.
  • Workflow-Integrität: übersprungene Freigabe, doppelte Ausführung, falsche Reihenfolge, parallele Anfrage, Retry nach teilweise ausgeführtem Seiteneffekt.
  • Datenabfluss: Secrets in Fehlermeldungen, PII in Logs, Daten in nicht autorisierten Exporten oder Caches.
  • KI-spezifische Grenzen: Prompt Injection, nicht erlaubte Tool-Nutzung, Argumente außerhalb des Schemas, fehlende menschliche Freigabe, Überschreitung eines Kosten- oder Aktionslimits.

Ein guter negativer Test prüft nicht nur einen Statuscode. Er prüft, dass der unerwünschte Seiteneffekt ausgeblieben ist.

Wenn eine Aktion eine Zahlung auslösen könnte, reicht 403 nicht als alleiniger Nachweis. Der Test sollte zusätzlich verifizieren, dass kein Zahlungsauftrag angelegt wurde. Bei einem blockierten KI-Tool-Aufruf sollte nicht nur eine Fehlermeldung entstehen; es darf kein HTTP-Request an das externe Tool gegangen sein.

it("führt ohne Freigabe keine Erstattung aus", async () => {
  const result = await refundService.request({
    orderId: "ord-42",
    amount: 12000,
    approvalId: undefined,
  });
 
  expect(result.code).toBe("APPROVAL_REQUIRED");
  expect(paymentProvider.refund).not.toHaveBeenCalled();
  expect(await refunds.countForOrder("ord-42")).toBe(0);
});

Das ist ein wichtiger Unterschied: Die Kontrolle muss nicht nur einen Fehler melden, sondern den gefährlichen Zustand verhindern.

Explizite Guards statt impliziter Annahmen

Sicherheitsrelevante Regeln sollten möglichst nahe am Seiteneffekt liegen und explizit lesbar sein.

Schlecht ist eine Architektur, in der die Berechtigung „wahrscheinlich“ schon im Controller geprüft wurde, bevor eine tiefere Service-Methode eine Löschung oder Überweisung ausführt. Solche Annahmen brechen bei neuen Endpunkten, Hintergrundjobs, CLI-Tools oder internen Integrationen.

Besser ist ein Guard an der Grenze:

async function deleteUser(
  actor: Actor,
  targetUserId: string,
  reason: string,
): Promise<void> {
  requireRole(actor, "user:delete");
  requireNonEmpty(reason, "reason");
  requireSameTenant(actor.tenantId, await users.tenantOf(targetUserId));
 
  await users.delete(targetUserId);
  await audit.log({
    type: "user.deleted",
    actorId: actor.id,
    targetUserId,
    reason,
  });
}

Ein Guard ist keine Garantie, wenn die Funktion umgangen werden kann. Deshalb gehören Architekturentscheidungen dazu:

  • Seiteneffekte hinter eine begrenzte Service-Schnittstelle legen.
  • Direkte Zugriffe auf Provider, Datenbanktabellen oder Admin-APIs einschränken.
  • Mandantenkontext aus vertrauenswürdigen Identitätsdaten ableiten.
  • Kritische Datenbankoperationen zusätzlich durch Constraints oder Row-Level Security absichern, wenn das Modell dazu passt.
  • Für besonders riskante Aktionen Freigaben, Limits und Idempotenz vorsehen.

Defense in Depth bedeutet nicht, dieselbe Prüfung dreimal zu kopieren. Es bedeutet, dass unterschiedliche Ebenen unterschiedliche Fehler auffangen: Anwendungscode, Datenbank, Identitätsanbieter, Netzwerk, Monitoring und organisatorischer Prozess.

Mutation Tests: Prüfen, ob der Test die Kontrolle wirklich bemerkt

Ein Negativtest kann vorhanden und dennoch wirkungslos sein. Das passiert etwa, wenn er den falschen Codepfad trifft, nur auf eine Fehlermeldung prüft oder zufällig wegen eines anderen Fehlers grün wird.

Mutation Testing hilft bei dieser Frage: Was geschieht, wenn wir eine Schutzbedingung absichtlich entfernen oder verändern?

Beispiele für gezielte Sicherheitsmutationen:

  • requireRole(actor, "user:delete") entfernen;
  • Tenant-Filter in einer Repository-Abfrage entfernen;
  • eine Allowlist durch eine Blocklist oder einen immer-wahren Ausdruck ersetzen;
  • die Token-Audience-Prüfung deaktivieren;
  • einen Approval-Check umgehen;
  • die Schema-Validierung von Tool-Argumenten aussetzen.

Wenn die Tests nach einer solchen Mutation weiterhin grün sind, liefern sie keinen belastbaren Nachweis für die Kontrolle.

Nicht jede Mutation-Test-Suite muss über das gesamte Repository laufen. Das wäre oft teuer und erzeugt wenig Signal. Praktischer ist eine gezielte Auswahl für sicherheitskritische Module: Autorisierung, Tenant-Isolation, Zahlungsfreigaben, Secret-Handling, Tool-Gateway und Auditierung.

Manuelle Mutationen sind ebenfalls wertvoll, besonders bei Architekturentscheidungen. Die Frage lautet dann nicht „Wie viele Prozent Mutation Score haben wir?“, sondern: Würde unser Nachweis auffallen, wenn diese konkrete Schutzlinie fehlt?

AI Safety: Das Modell ist kein Policy-Entscheider

Bei KI-gestützten Anwendungen verschiebt sich der Fehler häufig von „falsche Eingabe“ zu „unerwarteter Kontext“. Ein Modell kann durch Dokumente, E-Mails oder Webseiten angewiesen werden, seine Aufgabe umzudeuten. Es kann Tool-Aufrufe vorschlagen, die semantisch plausibel, aber für den konkreten Nutzer nicht erlaubt sind.

Die zentrale Regel lautet daher:

Modelloutput ist untrusted input, auch wenn er strukturiert aussieht.

Ein JSON-Tool-Call des Modells ist kein Berechtigungsnachweis. Der Tool-Gateway muss unabhängig prüfen:

  • Ist dieses Tool für diesen Workflow erlaubt?
  • Darf dieser Nutzer diese Aktion ausführen?
  • Passen die Argumente zu einem strikten Schema?
  • Gehört die referenzierte Ressource zum Tenant?
  • Überschreitet die Aktion ein Limit?
  • Ist eine menschliche Freigabe erforderlich?
  • Wird der Aufruf revisionsfähig protokolliert?

Ein negativer Test für einen KI-Agenten sollte deshalb nicht nur eine bösartige Prompt-Variante einspeisen. Er sollte nachweisen, dass ein verbotener Tool-Aufruf trotz entsprechender Modellantwort nicht ausgeführt wird.

it("blockiert nicht freigegebene Tool-Aufrufe aus Modelloutput", async () => {
  const modelOutput = {
    tool: "delete_customer",
    arguments: { customerId: "cust-99" },
  };
 
  const result = await toolGateway.execute(modelOutput, {
    actor: supportAgent,
    workflow: "answer_customer_question",
  });
 
  expect(result.code).toBe("TOOL_NOT_ALLOWED");
  expect(customerApi.delete).not.toHaveBeenCalled();
});

Der Test prüft eine Systemgrenze, nicht die Gutmütigkeit des Modells. Das ist der entscheidende Unterschied zwischen einer Demo und einem kontrollierbaren Produkt.

Beobachtbarkeit: Eine Kontrolle, die niemand sehen kann, ist schwer zu betreiben

Ein blockierter Angriff ist ein Erfolg. Wenn er nirgends sichtbar wird, fehlen aber wichtige Informationen:

  • Wird die Kontrolle tatsächlich ausgelöst?
  • Gibt es auffällige Häufungen?
  • Betrifft es einen Integrationsfehler, Missbrauch oder einen fehlerhaften Rollout?
  • Können wir einem Auditor erklären, wann und wie die Kontrolle wirksam war?

Sicherheitsrelevante Entscheidungen sollten daher strukturierte Ereignisse erzeugen, etwa authorization.denied, policy.blocked, approval.required oder tool_call.rejected.

Dabei gilt Datenminimierung. Ein Audit-Event benötigt selten den vollständigen Prompt, das gesamte Dokument oder ein Access Token. Meist reichen Zeitpunkt, Akteur, Aktion, Ressourcentyp, Entscheidung, Policy-Version und eine datensparsame Referenz auf die Ressource.

Beobachtbarkeit ersetzt keinen Schutzmechanismus. Ein Alert, der einen erfolgreichen Datenabfluss meldet, ist kein guter Primärschutz. Sie ergänzt den Guard durch Betriebsevidenz und ermöglicht die Reaktion auf neue Angriffsvarianten.

Compliance braucht nachvollziehbare Evidenz, nicht nur Dokumente

Compliance-Anforderungen werden oft als Dokumentationsaufgabe behandelt: Richtlinie schreiben, Kontrollkatalog ausfüllen, Screenshot ablegen. Das ist notwendig, aber nicht ausreichend.

Eine belastbare Kontrolle verbindet vier Artefakte:

  1. Anforderung: Was darf nicht passieren?
  2. Implementierung: Wo wird es technisch verhindert?
  3. Test: Welcher gezielte Gegenversuch zeigt die Wirkung?
  4. Betriebsevidenz: Wie ist die Kontrolle im laufenden Betrieb sichtbar und überwachbar?

Für kritische Regeln lohnt sich eine Traceability-Matrix. Sie muss nicht kompliziert sein. Eine Zeile pro Invariante mit Link auf Policy, Code-Modul, Test-ID, Dashboard oder Audit-Event reicht häufig aus.

Das hilft auch außerhalb formaler Audits. Bei einem Incident lässt sich schneller beantworten, ob eine Regel fehlte, falsch implementiert war, von einem neuen Pfad umgangen wurde oder ob Monitoring nicht gegriffen hat.

Ein pragmatischer Ablauf für Teams

Wer bestehende Systeme verbessern will, muss nicht zuerst alle Bedrohungen der Welt modellieren. Ein kleiner, wiederholbarer Ablauf ist wirksamer:

  1. Die zehn kritischsten „darf nicht passieren“-Sätze sammeln.
    Beginnen Sie bei Geld, Identitäten, Mandanten, Produktionszugriff, Secrets, personenbezogenen Daten und externen Seiteneffekten.

  2. Pro Satz einen konkreten verbotenen Pfad formulieren.
    Nicht „Zugriff muss sicher sein“, sondern „Nutzer eines anderen Tenants kann /invoices/{id} nicht lesen“.

  3. Kontrolle und Durchsetzungsort festlegen.
    Wer entscheidet? Welche vertrauenswürdigen Daten werden verwendet? Wo liegt der letzte Guard vor dem Seiteneffekt?

  4. Einen Negativtest auf Systemebene schreiben.
    Der Test muss den realistischen Umgehungsversuch ausführen und den ausbleibenden Seiteneffekt prüfen.

  5. Eine zentrale Kontrolle mutieren.
    Entfernen oder deaktivieren Sie den Guard temporär. Der Test muss fehlschlagen.

  6. Ein datensparsames Ereignis für Ablehnungen vorsehen.
    Nicht jede Ablehnung braucht einen Alarm. Aber kritische und gehäufte Ablehnungen sollten im Betrieb auffallen.

  7. Die Kontrolle bei neuen Schnittstellen wiederverwenden.
    Neue REST-Endpunkte, GraphQL-Resolver, Jobs, Importer und KI-Tools sind typische Stellen, an denen alte Annahmen verloren gehen.

Schluss: Kein Beweis für absolute Sicherheit, aber ein besserer Nachweis

Die Frage „Wie beweist man, dass etwas Gefährliches nicht passiert?“ hat keine einfache absolute Antwort. Offene Systeme, neue Angriffswege und Implementierungsfehler lassen sich nicht vollständig ausschließen.

Die falsche Konsequenz wäre, Sicherheitsanforderungen nur als Wunschformulierung zu behandeln.

Die bessere Konsequenz ist, Abwesenheit in überprüfbare Aussagen zu übersetzen: verbotene Pfade benennen, explizite Guards implementieren, negative Tests schreiben, Schutzlinien mutieren und Entscheidungen im Betrieb sichtbar machen.

Ein grüner Happy Path zeigt, dass das Produkt etwas kann. Ein gut getesteter und beobachtbarer negativer Pfad zeigt, dass es an einer kritischen Grenze auch bewusst nichts tut. Für Security Engineering, AI Safety, QA und Compliance ist genau das oft der wichtigere Nachweis.