Blog/Sicherheit & Compliance
Output-Guardrails für LLMs: Moderation, sicheres Rendern und Validierung, bevor etwas passiert
Was nach der Modellantwort zu prüfen ist: sicheres Markdown-Rendering, Schema-Prüfung vor Aktionen, Moderations-Klassifikatoren, Streaming und Groundedness.
Balázs Csorba··13 Min. Lesezeit
- LLM security
- Guardrails
- Content moderation
- OWASP
- Output validation

Das Wichtigste in Kürze
- OWASP LLM05:2025 behandelt Modellausgaben als nicht vertrauenswürdige Eingabe für das nächste System: Browser, Datenbank, Shell oder Mail-Vorlage.
- Die meisten echten Output-Vorfälle sind Rendering- und Aktionsfehler, nicht anstößiger Text. Zuerst Markdown bereinigen und Bilder und Links einschränken.
- Strukturierte Ausgaben gegen Schema und Allowlist prüfen, bevor sie eine Aktion auslösen; nie an eval, eine Shell oder zusammengesetztes SQL übergeben.
- Moderations-Klassifikatoren wie omni-moderation-latest oder Llama Guard 4 liefern Signale, keine Urteile; Schwellen und Aktionen legen Sie pro Kategorie fest.
- Streaming verschiebt den Kompromiss: Entweder sieht der Nutzer einen Chunk vor der Sperre, oder jeder Chunk kostet die Latenz der Prüfung.
Die meisten Diskussionen über Guardrails beginnen bei der Eingabe: Prompt Injection, Jailbreaks, Nutzer, die Dinge verlangen, die sie nicht bekommen sollen. Diese Hälfte ist wichtig, darüber habe ich in Abwehr von Prompt Injection geschrieben. Hier geht es um die andere Hälfte. Sobald das Modell geantwortet hat, landet sein Text irgendwo: in einer Chat-Blase, die als Markdown gerendert wird, in einem JSON-Parser, der eine Gutschrift auslöst, in einer Mail-Vorlage, in einer SQL-Abfrage. Jedes dieser Ziele hat eigene Regeln dafür, was sicher ist, und das Modell kennt keine davon.
Die OWASP Top 10 für LLM-Anwendungen nennen dieses Risiko LLM05:2025 Improper Output Handling. Im Folgenden beschreibe ich, wie ich Output-Prüfungen in produktiven Funktionen aufbaue: was wie gerendert wird, was vor einer Aktion validiert wird, wo Moderations-Klassifikatoren hingehören, was Streaming daran ändert und wie man zwischen Blockieren, Umschreiben und Eskalieren entscheidet.
Modellausgabe ist nicht vertrauenswürdige Eingabe für das nächste System
OWASP definiert Improper Output Handling als unzureichende Validierung, Bereinigung und Behandlung von Modellausgaben, bevor sie weitergereicht werden. Die Beispiele im Eintrag sind gewöhnliche Web-Schwachstellen mit einer neuen Quelle: Ausgaben, die an exec oder eval gehen, JavaScript oder Markdown, das der Browser als XSS ausführt, generiertes SQL ohne Parametrisierung, Dateipfade aus Ausgaben und Inhalte, die ohne Escaping in Mail-Vorlagen landen.
Diese Kategorie verdient ihren eigenen Platz, weil der Angreifer keinen Zugriff auf Ihre Ausgabe braucht. Mit indirekter Prompt Injection kann ein Dokument, eine Produktbewertung oder eine Webseite, die das Modell liest, steuern, was es schreibt. OWASP nennt genau das als Faktor, der die Auswirkung erhöht, neben Modellrechten, die über die des Endnutzers hinausgehen, und fehlender kontextabhängiger Kodierung. Die erste Gegenmaßnahme ist deutlich: das Modell wie jeden anderen Nutzer behandeln und auf seine Ausgaben Zero Trust anwenden.
Vier verschiedene Aufgaben, die alle Guardrails heißen
Ein Teil der Verwirrung um Output-Guardrails kommt daher, dass das Wort vier Aufgaben mit unterschiedlichen Werkzeugen und Zuständigkeiten abdeckt. Ich halte sie in Design-Reviews auseinander, weil ein Team, das eine Moderations-API eingekauft hat, oft glaubt, alle vier abgedeckt zu haben.
| Aufgabe | Beantwortete Frage | Typisches Werkzeug |
|---|---|---|
| Sicheres Rendern | Kann dieser Text beim Anzeigen Code ausführen oder Daten abfließen lassen? | Markdown-Renderer-Konfiguration, Sanitizer, CSP |
| Aktionsvalidierung | Ist dieser Tool-Aufruf oder dieses JSON erlaubt und wohlgeformt? | JSON Schema, Allowlists, Berechtigungsprüfung |
| Inhaltssicherheit | Ist das schädlich, hasserfüllt oder gegen die Richtlinie? | omni-moderation-latest, Llama Guard 4, Azure AI Content Safety |
| Korrektheit und Datenabfluss | Ist es belegt, und gibt es Daten preis, die es nicht soll? | Groundedness-Prüfung, PII- und Secret-Scanner |
Die Reihenfolge der Tabelle ist auch die Reihenfolge, in der ich die Arbeit priorisieren würde. Rendering- und Aktionsfehler machen aus einer einzelnen schlechten Antwort einen Vorfall; anstößiger Text in einem B2B-Support-Widget ist peinlich, löscht aber selten Daten.
Markdown so rendern, als hätte ein Angreifer es geschrieben
Chat-Oberflächen rendern Markdown, weil es gut aussieht. Markdown ist aber auch ein Weg, den Browser URLs abrufen zu lassen. Das klassische Exfiltrationsmuster: Eine eingeschleuste Anweisung bringt das Modell dazu, private Daten in den Query-String einer Bild-URL zu schreiben, und der Browser schickt sie dem Angreifer, sobald das Bild gerendert wird, ganz ohne Klick. Simon Willison hat das bei vielen Produkten dokumentiert; im Fall von GitLab Duo, gemeldet im Mai 2025, brachte eine versteckte Anweisung in einem öffentlichen Projekt den Assistenten dazu, einen privaten Merge Request zu lesen und eine Base64-Zusammenfassung über ein Bild-Tag auszuleiten. GitLab behob das, indem Links und Bilder nur noch auf vertrauenswürdige Domains zeigen dürfen.
In einem Vue- oder Nuxt-Frontend gelten die Regeln, die der Vue-Sicherheitsleitfaden schon für Nutzerinhalte nennt: nie nicht vertrauenswürdige Inhalte als Template verwenden, v-html nicht mit unbereinigtem HTML nutzen und URLs auf dem Server bereinigen. Für Modellausgaben mache ich vier Dinge: rohes HTML im Markdown-Renderer abschalten, Bilder und Links mit Hosts außerhalb einer Allowlist umschreiben oder verwerfen, verbleibenden externen Links rel="noopener nofollow" geben und eine Content Security Policy senden, deren img-src nur eigene Hosts erlaubt. Die CSP ist das Netz, das auch dann noch hält, wenn der Renderer einen Fehler hat.
// server/utils/render-answer.ts — Markdown from the model, rendered on the server
import MarkdownIt from 'markdown-it'
const ALLOWED_HOSTS = new Set(['balazscsorba.com', 'cdn.example-shop.at'])
const md = new MarkdownIt({ html: false, linkify: false })
function allowed(url: string) {
try {
const u = new URL(url, 'https://balazscsorba.com')
return (u.protocol === 'https:' && ALLOWED_HOSTS.has(u.hostname))
} catch { return false }
}
// images to unknown hosts become plain alt text, so nothing is fetched
md.renderer.rules.image = (tokens, i) => {
const src = tokens[i].attrGet('src') ?? ''
return allowed(src) ? `<img src="${md.utils.escapeHtml(src)}" alt="${md.utils.escapeHtml(tokens[i].content)}">`
: md.utils.escapeHtml(tokens[i].content)
}
const defaultLink = md.renderer.rules.link_open ?? ((t, i, o, e, s) => s.renderToken(t, i, o))
md.renderer.rules.link_open = (tokens, i, opts, env, self) => {
if (!allowed(tokens[i].attrGet('href') ?? '')) tokens[i].attrSet('href', '#blocked-link')
tokens[i].attrSet('rel', 'noopener nofollow')
return defaultLink(tokens, i, opts, env, self)
}
export const renderAnswer = (text: string) => md.render(text)
Auf dem Server rendern und HTML erst an den Client schicken, nachdem diese Regeln gelaufen sind. Wer im Browser rendert, wendet dort dieselben Regeln an und behält trotzdem die CSP, denn eine einzige Komponente, die bei roher Ausgabe auf v-html zurückfällt, macht den Rest zunichte.
Strukturierte Ausgaben validieren, bevor sie etwas auslösen
Wenn eine Ausgabe eine Aktion steuert, also einen Tool-Aufruf, eine Gutschrift oder eine Datenbankabfrage, sind die Prüfungen deterministisch und billig, es gibt also keinen Grund, sie wegzulassen. Der strikte JSON-Schema-Modus garantiert die Form, nicht die Bedeutung, wie ich in Structured Outputs mit JSON Schema erklärt habe. Nach dem Parsen die Werte gegen Geschäftsregeln prüfen: Darf der aktuelle Nutzer diese Bestellnummer sehen, liegt die Gutschrift unter dem Limit, das der Assistent selbst gewähren darf, gibt es diese SKU im Katalog.
- Ausgaben nie an
eval, eine Shell oder eine Template-Engine übergeben, die Ausdrücke ausführt. - Für jeden Wert aus dem Modell parametrisierte Abfragen verwenden; OWASP nennt das ausdrücklich.
- Dateinamen und IDs über eine Nachschlagetabelle auflösen, statt Pfade aus Text zu bauen.
- Ausgaben für den Zielkontext escapen: HTML, Mail, CSV, Shell-Argument.
- Vor irreversiblen Aktionen einen Menschen einbinden, wie bei Freigabestufen mit Human in the Loop.
Speziell bei generiertem SQL zählen die Read-only-Rolle und der Abfrage-Validator mehr als jeder Klassifikator; Details dazu in Guardrails für Text-to-SQL.
Moderations-Klassifikatoren: was die gängigen Optionen prüfen
Für Inhaltssicherheit schreibt man selten ein eigenes Modell. Drei Familien decken die meisten Projekte ab, die ich sehe: den OpenAI-Moderation-Endpunkt, Meta Llama Guard 4 als Open-Weights-Modell zum Selbsthosten oder Mieten und Azure AI Content Safety mit mehreren spezialisierten Detektoren.
| Option | Was es klassifiziert | Hinweise aus der Doku |
|---|---|---|
| OpenAI omni-moderation-latest | Text und Bilder; Kategorien wie Belästigung, Hass, Illegales, Selbstverletzung, Sexuelles, Gewalt | Kostenlos. Viele Kategorien nur für Text. Kann inline in einer Generierungsanfrage laufen. |
| Meta Llama Guard 4 (12B) | Prompts und Antworten gegen Gefahrenkategorien S1 bis S14 | Open Weights, Text und Bilder. Gibt safe oder unsafe plus verletzte Kategorien aus. Kategorien im Prompt anpassbar. |
| Azure AI Content Safety | Schadenskategorien, Prompt Shields, Groundedness, geschütztes Material | Erkennung geschützten Materials nur auf Englisch; der Code-Index reicht nur bis April 2023. |
Llama Guard 4 nutzt die Gefahrentaxonomie von MLCommons mit einer Ergänzung: S1 Gewaltverbrechen, S2 gewaltlose Verbrechen, S3 Sexualverbrechen, S4 Ausbeutung von Kindern, S5 Verleumdung, S6 Fachberatung, S7 Privatsphäre, S8 geistiges Eigentum, S9 Massenvernichtungswaffen, S10 Hass, S11 Selbstverletzung, S12 sexuelle Inhalte, S13 Wahlen und S14 Missbrauch von Code-Interpretern. Um eine Antwort zu prüfen, muss der Prompt laut Meta sowohl die Nutzernachricht als auch die Antwort des Agenten enthalten, weil die Frage der Kontext für die Bewertung ist. Die erste Ausgabezeile ist safe oder unsafe, die zweite nennt die verletzten Kategorien.
OpenAI hat Inline-Moderation ergänzt: Mit einem moderation-Objekt auf oberster Ebene und omni-moderation-latest in einer Responses- oder Chat-Completions-Anfrage erhält man Werte für Eingabe und Ausgabe, ohne zweiten Aufruf. Die Doku ist in zwei Punkten deutlich, die ich jedem Team mitgeben würde. Eine sicherheitsbewusste Ablehnung kann trotzdem markiert werden, weil sie schädliche Inhalte bespricht, also sind die Werte Signale für die eigene Richtlinie und keine automatische Sperre. Und bei Tool-Aufrufen deckt die Moderation Argumente und Ausgaben im Gespräch ab, nicht Tool-Namen, Beschreibungen oder Schemas.
Streaming: zuerst anzeigen oder zuerst prüfen
Nutzer erwarten, dass Tokens während der Generierung erscheinen, und jede Output-Prüfung will den ganzen Text. Beides zugleich geht nicht, und die Werkzeuge sagen das auch. OpenAI schreibt, dass Inline-Moderationswerte erst nach Vorliegen der vollständigen Ausgabe kommen, nicht mit den Deltas. NVIDIA NeMo Guardrails führt Output Rails bei aktiviertem Streaming auf Chunks aus: chunk_size ist standardmäßig 200 Tokens, context_size übernimmt 50 Tokens in den nächsten Chunk, damit ein Satz über eine Grenze hinweg als Ganzes bewertet wird, und stream_first bestimmt die Reihenfolge.
stream_first: True erhält der Client jeden Chunk vor der Prüfung; mit False läuft die Prüfung zuerst und addiert ihre Latenz zu jedem Chunk.Mit stream_first auf true, dem Standard, kann der Nutzer einen Chunk bereits gelesen haben, der dann gesperrt wird, und der Stream endet mit einem JSON-Fehler, den das Frontend behandeln muss, am besten indem es die Blase ersetzt, statt einen halben Satz stehen zu lassen. Mit false wird nie etwas Gesperrtes angezeigt, aber jeder Chunk wartet auf die Prüfung. Meine Regel: zuerst streamen bei Assistenten mit geringem Risiko, bei denen der schlimmste Fall ein unglücklicher Satz ist; zuerst prüfen, wenn Ausgaben personenbezogene Daten, Preise oder Zusagen enthalten können, an die das Unternehmen gebunden wäre.
Prüfung auf Belegbarkeit und Datenabfluss
In einem RAG-Assistenten ist der teuerste Output-Fehler eine selbstsichere Antwort, die die Quellen nicht stützen. Azure AI Content Safety bietet dafür Groundedness Detection: Man schickt den generierten Text und die Quellen, wählt eine Domain wie Medical oder Generic, und der Dienst markiert unbelegte Inhalte. Ein optionaler Korrekturmodus liefert an die Quellen angepassten Text zurück. Ich nutze solche Prüfungen eher für Stichproben und Alarme als als harte Sperre, weil ein Grounding-Richter selbst ein fehlerbehaftetes Modell ist und die eigentliche Lösung weiter vorne bei Retrieval und Zitaten liegt, beschrieben in Grounding und Zitate.
Datenabfluss lässt sich leichter deterministisch prüfen. Ausgaben nach bekannten Mustern durchsuchen: IBANs, Mail-Adressen und Telefonnummern anderer Personen als des aktuellen Nutzers, API-Keys, interne Hostnamen und Fragmente des eigenen System-Prompts. Dieselben Detektoren, die man auf der Eingabeseite für PII-Schwärzung nutzt, funktionieren auch auf der Ausgabeseite; der Unterschied ist, dass man bei Ausgaben meist schwärzt und protokolliert, statt die ganze Antwort zu verwerfen.
Blockieren, schwärzen, umschreiben oder eskalieren
Eine Guardrail ist eine Entscheidung, kein Wert. Für jede Kategorie festhalten, was passiert, wenn sie anschlägt, und wer es sieht. Mit dieser Tabelle beginne ich und justiere sie dann mit echtem Traffic.
| Befund | Aktion | Nutzer sieht |
|---|---|---|
| Unsicherer Link- oder Bild-Host | In Text umschreiben, protokollieren | Antwort ohne Link |
| Klarer Verstoß gegen Inhaltssicherheit | Blockieren, durch festen Text ersetzen, Werte protokollieren | Neutrale Ablehnung und Kontaktmöglichkeit |
| PII oder Secret einer anderen Person | Schwärzen, Security alarmieren | Antwort mit maskiertem Wert |
| Ungültige Tool-Argumente | Ablehnen, Fehler einmal an das Modell zurückgeben | Nichts, außer der Retry scheitert |
| Unbelegte Aussage in einer RAG-Antwort | Zur Prüfung markieren, Stichprobe | Antwort mit Quellen wie gewohnt |
Zwei Gewohnheiten halten das ehrlich. Für jede Entscheidung Klassifikator-Version, Werte und Aktion protokollieren, weil Schwellen driften, wenn der Anbieter ein Modell hinter einem -latest-Alias aktualisiert. Und Nutzern beim Blockieren etwas Brauchbares zeigen: Eine feste Nachricht mit einem Weg zu einem Menschen ist besser als eine stumme leere Blase, die Leute für einen Fehler halten und erneut versuchen.
Alles zusammen in einer Server-Route
Billige deterministische Prüfungen laufen zuerst, Klassifikatoren danach und das Rendering zuletzt, damit die teuren und fehlbaren Schritte so wenig wie möglich sehen. In Nuxt oder jedem Node-Backend ist die Variante ohne Streaming eine kurze Funktion.
// server/utils/guard-output.ts
import { renderAnswer } from './render-answer'
import { redactLeaks } from './redact' // your PII / secret patterns
const BLOCKED = 'I cannot help with that here. Please contact our support team.'
export async function guardOutput(text: string) {
const { text: clean, findings } = redactLeaks(text)
const res = await fetch('https://api.openai.com/v1/moderations', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.OPENAI_API_KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ model: 'omni-moderation-latest', input: clean }),
})
if (!res.ok) throw new Error(`moderation failed: ${res.status}`) // fail closed
const result = (await res.json()).results[0]
const decision = result.flagged ? 'block' : findings.length ? 'redact' : 'allow'
console.info(JSON.stringify({ event: 'output_guard', decision, findings, scores: result.category_scores }))
return { html: renderAnswer(decision === 'block' ? BLOCKED : clean), decision }
}
Zwei Entscheidungen in dieser Skizze sind bewusst getroffen. Sie schlägt geschlossen fehl, wenn der Moderationsaufruf scheitert, was für einen öffentlichen Assistenten richtig ist und für ein internes Werkzeug, bei dem Verfügbarkeit wichtiger ist, falsch; entscheiden Sie das ausdrücklich. Und sie nutzt flagged als erste Entscheidung, wie OpenAI vorschlägt, protokolliert aber jeden Wert, damit man später auf Schwellen pro Kategorie umstellen kann. Wer ein Framework bevorzugt: Guardrails AI verpackt dieselbe Idee in Validatoren, die man an einen Modellaufruf hängt, siehe meinen Test von Guardrails AI.
Guardrails testen wie jede andere Funktion
Guardrails versagen in zwei Richtungen, und beide kosten Geld. Übersehene Verstöße sind die offensichtliche. Fehlalarme sind die leise: Ein Support-Assistent, der Fragen zum „Beenden eines Prozesses“ oder zum „Versand von Gefahrgut“ ablehnt, verliert Nutzer, ohne dass es jemand merkt. Ein kleines gelabeltes Set mit beiden Arten aufbauen, einschließlich Fachvokabular, das Klassifikatoren stolpern lässt, und es in der CI laufen lassen, sobald sich eine Schwelle, ein Prompt oder eine Klassifikator-Version ändert.
- Exfiltrations-Payloads aufnehmen: Bild-URLs mit Query-Strings, Links auf ähnlich aussehende Domains, HTML-Tags in Markdown.
- Tool-Aufrufe mit Werten außerhalb des Bereichs und IDs anderer Kunden aufnehmen.
- Sperrquote und Fehlalarmquote pro Kategorie auf Produktionsstichproben verfolgen, nicht nur im Testset.
- Die ganze Kette mit einem Werkzeug wie Promptfoo red-teamen, denn eine Payload, die eine Schicht passiert, ist erst interessant, wenn sie alle passiert.
Output-Guardrails sind unspektakuläre Ingenieursarbeit: Renderer-Konfiguration, Schemas, ein Klassifikator-Aufruf und eine Entscheidungstabelle. Genau deshalb funktionieren sie. Das Modell wird Sie weiter überraschen; die Systeme danach sollten es nicht.
Quellen
- OWASP GenAI Security Project: LLM05:2025 Improper Output Handling
- OpenAI API Docs: Moderation
- Meta: Llama Guard 4 model card and prompt formats
- NVIDIA NeMo Guardrails: Output rail streaming configuration
- Microsoft Learn: Prompt Shields in Azure AI Content Safety
- Microsoft Learn: Groundedness detection in Azure AI Content Safety
- Microsoft Learn: Protected material detection in Azure AI Content Safety
- Guardrails AI documentation
- Vue.js docs: Security best practices
- Simon Willison: Remote prompt injection in GitLab Duo leads to source code theft
Häufige Fragen
Was ist Improper Output Handling?
Das ist der Eintrag LLM05:2025 der OWASP LLM Top 10: unzureichende Validierung, Bereinigung und Behandlung von Modellausgaben, bevor sie an andere Komponenten gehen. Typische Folgen sind XSS im Browser, SQL-Injection, Path Traversal oder Remote Code Execution im Backend.
Reicht der OpenAI-Moderation-Endpunkt als Output-Guardrail?
Er deckt Kategorien der Inhaltssicherheit ab, etwa Hass, Belästigung, Selbstverletzung, sexuelle Inhalte, Gewalt und Anleitungen zu illegalen Handlungen, und ist kostenlos. Er prüft nicht, ob HTML sicher gerendert werden kann, ob eine Antwort belegt ist oder ob ein Tool-Aufruf erlaubt ist. Er ist eine Schicht von mehreren.
Kann ich eine gestreamte Antwort moderieren?
Ja, aber Sie müssen sich entscheiden. OpenAI liefert Inline-Moderationswerte erst, wenn die vollständige Ausgabe vorliegt. NeMo Guardrails prüft standardmäßig Chunks von 200 Tokens und streamt jeden Chunk entweder vor der Prüfung oder hält ihn zurück, bis sie bestanden ist.
Soll ich markierte Ausgaben blockieren oder umschreiben?
Das hängt von der Kategorie ab. Bei klaren Sicherheitsverstößen blockieren und durch eine feste Nachricht ersetzen, bei geleakten personenbezogenen Daten oder Secrets schwärzen, Grenzfälle und geschäftskritische Fälle an einen Menschen geben. Jede Entscheidung mit den Werten protokollieren, um Schwellen nachzujustieren.
Wie verhindere ich Exfiltration über Markdown-Bilder?
Keine Bilder oder Links auf beliebige Domains aus Modellausgaben rendern. Eine Allowlist von Hosts verwenden, alles andere serverseitig entfernen oder umschreiben, rohes HTML im Markdown-Renderer abschalten und eine strikte Content Security Policy setzen.