Discord-Kanäle leiten interne Payloads weiter - Discord Channel Leaking Internal Payloads: EXTERNAL_UNTRUSTED_CONTENT Wrappers in User Messages
Interne Wrapper-Marker und fehlerhaft extrahierte Anhangstexte werden an Discord-Kanäle weitergeleitet, anstatt vor der Übertragung bereinigt zu werden.
🔍 Symptome
Beobachtete benutzerseitige Fehler
Bei der Interaktion mit dem Assistenten über Discord beobachten Benutzer Nachrichten, die rohe interne Inhalte enthalten, die niemals die Präsentationsebene erreichen sollten. Die offengelegten Inhalte manifestieren sich in zwei unterschiedlichen Mustern:
Muster 1: Wrapper-Syntax-Leckage
Nachrichten enthalten rohe Serialisierungsmarker direkt im Discord-Chat:
<<<EXTERNAL_UNTRUSTED_CONTENT id="msg_abc123">>>
Source: External
UNTRUSTED Discord message body
<<<END_EXTERNAL_UNTRUSTED_CONTENT id="msg_abc123">>>
Muster 2: Spam mit malformed Anhang-Payload
Große Textblöcke mit unsinnigem Inhalt, dominiert von wiederholten technischen Begriffen:
attach attachment attachment hookup toggle compiler
attachment hookup toggle compiler attach attachment
UNTRUSTED Discord message body Source External Source External
attach attachment attachment hookup toggle compiler
Technische Manifestationen
| Komponente | Manifestation |
|---|---|
| Discord-Transport | Rohe Wrapper-Tags erscheinen in ausgehenden Nachrichten-Payloads |
| Anhang-Handler | Korrupte Extraktionsergebnisse werden an den Kanal weitergeleitet |
| Asynchrone Tool-Vervollständigung | Eingereihte Vervollständigungstexte enthalten interne Marker |
| Sanitisierungsschicht | Grenzendurchsetzungsfehler zwischen Kontext und Rendering |
Auslösebedingungen
Das Problem tritt nach einer der folgenden Operationen auf:
- Der Assistent verarbeitet eine Nachricht mit Anhängen
- Asynchrone Tool-Vervollständigung liefert Ergebnisse an den Discord-Kanal
- Externe Inhalte werden durch das
EXTERNAL_UNTRUSTED_CONTENT-Wrapper-System verarbeitet - Mehrfach-Gespräch mit Datei-/Bildanhängen
🧠 Ursache
Architektonische Fehlerpunkte
Die Leckage deutet auf einen Sanitisierungsgrenzen-Fehler in der Nachrichtenpipeline zwischen interner Verarbeitung und Discord-Transport hin. Das OpenClaw-Framework verwendet den EXTERNAL_UNTRUSTED_CONTENT-Wrapper, um nicht vertrauenswürdige Benutzerinhalte während der Agent-Verarbeitung zu isolieren. Dieser Wrapper sollte:
- Intern während der Kontextzusammenstellung verbraucht werden
- Niemals an ausgehende Transport-Schichten serialisiert werden
- Entfernt werden, bevor eine Nachricht die Rendering-Pipeline erreicht
Fehlersequenz
┌─────────────────────────────────────────────────────────────────┐
│ MESSAGE FLOW (FAILING) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Discord Message Received │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Content Wrapper │ ← EXTERNAL_UNTRUSTED_CONTENT added │
│ │ Injection │ to isolate untrusted input │
│ └────────┬────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Agent Runtime │ ← Wrapper consumed in context │
│ │ Processing │ (intended behavior) │
│ └────────┬────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Discord Transport│ ← SANITIZATION FAILURE │
│ │ Renderer │ Wrapper not stripped before posting │
│ └────────┬────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ RAW WRAPPER + │ ← User sees: │
│ │ Payload │ <<>> │
│ │ Forwarded │ UNTRUSTED Discord message body │
│ └─────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Code-Pfad-Analyse
Der Defekt existiert im Discord-Transport-Adapter, wo die Antwortnachricht konstruiert wird. Der erwartete Code-Pfad:
// CORRECT FLOW (Expected)
function buildDiscordMessage(agentResponse) {
const sanitized = sanitize(s剥离所有内部标记);
const message = createDiscordEmbed(sanitized);
return message;
}
// ACTUAL FLOW (Defective)
function buildDiscordMessage(agentResponse) {
// Sanitization missing or ineffective
const message = createDiscordEmbed(agentResponse.raw);
// Raw EXTERNAL_UNTRUSTED_CONTENT markers included
return message;
}
Anhang-Payload-Korruption
Das “Mülltext”-Muster resultiert aus der Anhang-Text-Extraktion, wobei:
- Binäre oder malformed Anhangsdaten verarbeitet werden
- Die Extraktion korrupte Unicode/Code-Point-Sequenzen produziert
- Diese Sequenzen während der Mehrfach-Anhang-Verarbeitung wiederholt werden
- Die korrupte Payload die Inhaltsfilterung umgeht
Subsystem-Verantwortlichkeiten
| Subsystem | Erwartetes Verhalten | Tatsächliches Verhalten |
|---|---|---|
DiscordTransport | Interne Wrapper vor dem Posten entfernen | Leitet rohen Inhalt weiter |
ContentSanitizer | EXTERNAL_* Marker entfernen | Filter deaktiviert oder umgangen |
AttachmentHandler | Saubere Extraktionstexte | Leitet korrupte Payload weiter |
AsyncCompletionRouter | Liefert saubere Vervollständigung | Enthält Debug-Marker |
🛠️ Schritt-für-Schritt-Lösung
Phase 1: Wrapper-Propagation im Discord-Transport deaktivieren
Datei: src/transports/discord/index.ts (oder äquivalentes Transportmodul)
Vorher (Fehlerhaft):
async function handleAssistantMessage(message: ProcessedMessage): Promise<void> {
const discordMessage = {
content: message.content,
embeds: message.embeds
};
await this.client.sendMessage(discordMessage);
}
Nachher (Korrigiert):
async function handleAssistantMessage(message: ProcessedMessage): Promise<void> {
const sanitizedContent = this.sanitizeForDiscord(message.content);
const discordMessage = {
content: sanitizedContent,
embeds: message.embeds
};
await this.client.sendMessage(discordMessage);
}
private sanitizeForDiscord(content: string): string {
// Remove all internal wrapper markers
const patterns = [
/<<<EXTERNAL_UNTRUSTED_CONTENT[^>]*>>>/gi,
/<<<END_EXTERNAL_UNTRUSTED_CONTENT[^>]*>>>/gi,
/<<<INTERNAL_[A-Z_]+>>>/gi,
/Source:\s*(External|Internal)/gi
];
let sanitized = content;
for (const pattern of patterns) {
sanitized = sanitized.replace(pattern, '');
}
return sanitized.trim();
}
Phase 2: Anhang-Extraktions-Sanitisierung verstärken
Datei: src/handlers/attachment-extractor.ts
Vorher (Fehlerhaft):
function extractTextFromAttachment(attachment: Attachment): string {
const raw = processAttachmentBinary(attachment);
return raw.text || '';
}
Nachher (Korrigiert):
function extractTextFromAttachment(attachment: Attachment): string {
const raw = processAttachmentBinary(attachment);
let text = raw.text || '';
// Discard malformed extractions (repeated tokens indicate corruption)
if (isMalformedExtraction(text)) {
console.warn(`[Sanitizer] Discarding malformed attachment extraction for ${attachment.id}`);
return '';
}
// Strip any internal markers that slipped through
text = stripInternalMarkers(text);
// Limit length to prevent spam
const MAX_LENGTH = 4000;
if (text.length > MAX_LENGTH) {
text = text.substring(0, MAX_LENGTH) + '\n[Attachment content truncated]';
}
return text;
}
function isMalformedExtraction(text: string): boolean {
// Detect repeated token patterns indicating extraction failure
const tokens = text.toLowerCase().split(/\s+/);
const uniqueRatio = new Set(tokens).size / tokens.length;
// If <20% unique tokens, extraction is likely corrupted
return uniqueRatio < 0.2 && tokens.length > 50;
}
Phase 3: Asynchrone Tool-Vervollständigungs-Routing korrigieren
Datei: src/routing/async-completion-router.ts
Vorher (Fehlerhaft):
async function forwardCompletion(result: ToolResult): Promise<void> {
const message = buildChannelMessage(result);
await this.transport.post(message);
}
Nachher (Korrigiert):
async function forwardCompletion(result: ToolResult): Promise<void> {
// Ensure clean payload before routing
const cleanPayload = this.sanitizer.sanitize(result.payload);
if (cleanPayload.isDirty) {
console.error('[Router] Sanitizer detected dirty payload in async completion');
// Log for debugging, but still deliver cleaned content
}
const message = buildChannelMessage({
...result,
payload: cleanPayload.content
});
await this.transport.post(message);
}
Phase 4: Transport-Level-Guard hinzufügen
Datei: src/transports/discord/client.ts
Einen finalen Sanitisierungs-Gate vor jedem Discord-API-Aufruf hinzufügen:
async sendMessage(message: DiscordMessage): Promise<API.Message> {
// Final safety net - ensure no internal content escapes
const finalContent = this.stripInternalMarkers(message.content);
if (finalContent !== message.content) {
logger.warn('[DiscordTransport] Stripped internal markers before send');
}
// Hard block if wrapper syntax detected (indicates serious leak)
if (this.containsWrapperSyntax(finalContent)) {
logger.error('[DiscordTransport] CRITICAL: Wrapper syntax detected at send time');
throw new Error('SANITATION_FAILURE: Internal content detected in outbound message');
}
return this.api.createMessage(this.channelId, {
content: finalContent,
embeds: message.embeds
});
}
private containsWrapperSyntax(text: string): boolean {
return /<<<[A-Z_]+>>>/.test(text);
}
🧪 Verifizierung
Testfall 1: Wrapper-Marker-Entfernung
Führen Sie die Sanitisierungsfunktion gegen bekannte interne Inhalte aus:
const { sanitizeForDiscord } = require('./src/transports/discord/sanitizer');
const testCases = [
{
input: '<<>>UNTRUSTED Discord message body<<>>',
expected: 'UNTRUSTED Discord message body'
},
{
input: 'Source: External\nUser message\nSource: Internal',
expected: 'User message'
},
{
input: '<<>>\nValid response\n<<>>',
expected: 'Valid response'
}
];
let passed = 0;
for (const { input, expected } of testCases) {
const result = sanitizeForDiscord(input);
if (result === expected) {
console.log('✅ PASS:', JSON.stringify(result));
passed++;
} else {
console.log('❌ FAIL:', JSON.stringify({ input, expected, got: result }));
}
}
console.log(`\nResults: ${passed}/${testCases.length} tests passed`);
process.exit(passed === testCases.length ? 0 : 1);
Erwartete Ausgabe:
✅ PASS: "UNTRUSTED Discord message body"
✅ PASS: "User message"
✅ PASS: "Valid response"
Results: 3/3 tests passed
Testfall 2: Ende-zu-Ende Discord-Transport-Test
// Integration test - requires mock Discord client
const { DiscordTransport } = require('./src/transports/discord');
const mockClient = {
messages: [],
async sendMessage(msg) {
this.messages.push(msg);
return { id: 'test-' + Date.now() };
}
};
const transport = new DiscordTransport(mockClient);
// Simulate message with internal markers
const dirtyMessage = {
content: '<<>>Corrupted payload<<>>',
embeds: []
};
try {
await transport.handleAssistantMessage(dirtyMessage);
const sent = mockClient.messages[0];
if (sent.content.includes('<<<')) {
console.log('❌ FAIL: Wrapper syntax leaked to Discord');
console.log('Sent content:', sent.content);
process.exit(1);
}
console.log('✅ PASS: Message sanitized before Discord send');
console.log('Final content:', sent.content);
} catch (e) {
if (e.message.includes('SANITATION_FAILURE')) {
console.log('✅ PASS: Hard block triggered on dirty content');
} else {
throw e;
}
}
Testfall 3: Malformed Anhang-Erkennung
const { isMalformedExtraction } = require('./src/handlers/attachment-extractor');
// Corrupted payload (high repetition)
const corrupted = Array(200).fill('attach attachment hookup toggle compiler').join(' ');
console.log('Corrupted detection:', isMalformedExtraction(corrupted)); // Should be true
// Valid text
const valid = 'User uploaded a document containing meeting notes from Tuesday.';
console.log('Valid detection:', isMalformedExtraction(valid)); // Should be false
Erwartete Ausgabe:
Corrupted detection: true
Valid detection: false
Verifizierungs-Checkliste
Nach Anwendung der Korrekturen bestätigen:
- Keine
<<<EXTERNAL_UNTRUSTED_CONTENT-Strings in Discord-Nachrichtenverlauf - Keine
<<<END_EXTERNAL_UNTRUSTED_CONTENT-Strings in Discord-Nachrichtenverlauf - Keine
Source: External/Source: Internalerscheinen in benutzer sichtbaren Nachrichten - Aus Anhängen extrahierte Texte enthalten keine repetitiven Token-Muster (<20% Unique-Ratio)
- Unit-Tests für
sanitizeForDiscord-Funktion bestanden - Integrationstests für Discord-Transport bestanden
- Hard Block löst Fehler aus, wenn Wrapper-Syntax zur Sendezeit erkannt wird
⚠️ Häufige Fehler
Umgebungsspezifische Fallen
Docker-Container-Isolation
Wenn OpenClaw in Docker läuft, stellen Sie sicher, dass das Sanitisierungsmodul korrekt gemountet ist und nicht durch ein Volume überschrieben wird, das auf die fehlerhafte Version zurückfällt:
# Wrong - local source overrides container
docker run -v $(pwd)/src:/app/src openclaw:latest
# Correct - use container's fixed source
docker run openclaw:latest
Windows-Zeilenenden
Der Wrapper-Regex kann fehlschlagen, wenn Inhalte \r\n-Zeilenenden enthalten. Stellen Sie sicher, dass die Sanitisierung beide behandelt:
// BROKEN: Only matches Unix line endings
const pattern = /<<<EXTERNAL_UNTRUSTED_CONTENT[^>]*>>>/g;
// FIXED: Handles both Windows and Unix
const pattern = /<<<EXTERNAL_UNTRUSTED_CONTENT[^>\r\n]*>>>/gi;
Node.js-Versionsinkompatibilitäten
Der Set-Konstruktor für die Unique-Ratio-Berechnung erfordert Node.js 12+. Überprüfen Sie die Kompatibilität:
// Feature detection fallback
const uniqueRatio = typeof Set !== 'undefined'
? new Set(tokens).size / tokens.length
: [...new Set(tokens)].length / tokens.length;
Konfigurations-Fallen
Sanitisierung durch Umgebungsvariable deaktiviert
Einige Deployments deaktivieren Sanitisierung zum Debuggen, was dieses Leck verursacht:
# .env file - ensure sanitization is NOT disabled
SANITIZATION_ENABLED=true
# SANITIZATION_ENABLED=false ← REMOVE OR SET TO TRUE
Transport-Konfiguration erbt keinen Base-Sanitizer
Wenn Sie eine benutzerdefinierte Discord-Transport-Implementierung verwenden, stellen Sie sicher, dass sie den Base-ContentSanitizer erbt:
// WRONG: Custom transport bypasses sanitization
class DiscordTransportCustom {
async send(msg) { /* direct send without sanitization */ }
}
// CORRECT: Inherit sanitization
class DiscordTransportCustom extends BaseTransport {
async send(msg) {
return super.send(this.sanitizer.sanitize(msg));
}
}
Runtime-Edge-Cases
Unicode-Normalisierungsangriffe
Bösartige Inhalte können Unicode-Lookalike-Zeichen verwenden, um Mustervergleiche zu umgehen:
// Attempted bypass: Cyrillic 'а' instead of Latin 'a'
const malicious = '<<<ЕXTERNAL_UNTRUSTED_CONTENT id="1">>>'; // Different chars
// Defensive: Normalize before pattern matching
const normalized = content.normalize('NFKC');
const sanitized = stripInternalMarkers(normalized);
Race Condition bei gleichzeitiger Nachrichten-Sanitisierung
Wenn mehrere asynchrone Tool-Vervollständigungen gleichzeitig ausgelöst werden:
// Ensure thread-safe sanitization by not mutating shared state
// WRONG: Mutates input in place
function sanitize(content) {
content = content.replace(pattern1, '');
return content.replace(pattern2, ''); // Returns mutated original
}
// CORRECT: Immutable operations
function sanitize(content) {
return content
.replace(pattern1, '')
.replace(pattern2, '');
}
Leeres Sanitisierungsergebnis
Wenn Sanitisierung alle Inhalte entfernt, stellen Sie sicher, dass die Nachricht nicht gesendet wird (vermeidet leeren Spam):
const sanitized = stripInternalMarkers(raw);
if (!sanitized.trim()) {
logger.warn('[Discord] Sanitization produced empty message, discarding');
return; // Do not post to Discord
}
🔗 Zugehörige Fehler
Direkt zugehörige Probleme
| Fehler/Problem | Beschreibung | Verbindung |
|---|---|---|
EXTERNAL_UNTRUSTED_CONTENT Wrapper-Leck | Rohe interne Marker für Benutzer sichtbar | Primäres Problem - identisches Symptom |
| Anhang-Text-Extraktionskorruption | Müll-/malformed Text von Anhängen | Gleiche Ursache: fehlende Sanitisierungsgrenze |
| Asynchroner Tool-Vervollständigungs-Spam | Duplizierte/kaputte Vervollständigungen in Kanälen | Teilt Transport-Rendering-Defekt |
| Discord-Rate-Limit-Fehler | Kann auftreten, wenn Leck Nachrichten-Spam-Schleife verursacht | Sekundäres Symptom von Müllinhalten |
| Nachrichtenwarteschlangen-Backup | Wenn Transport wiederholt an schmutzigem Inhalt scheitert | Nachgelagerte Konsequenz von unsanitisiertem Input |
Historisch zugehörige Probleme
| Issue ID | Titel | Relevanz |
|---|---|---|
| GH-XXX | Sanitizer not applied to async completion payloads | Direkter Vorgänger - Korrektur nicht auf alle Pfade propagiert |
| GH-YYY | Discord transport bypasses content filtering in dev mode | Umgebungsspezifische Variante des Grenzfehlers |
| GH-ZZZ | Attachment extraction returning binary garbage | Gleicher Korruptionsmechanismus, anderes Subsystem |
| GH-AAA | Internal wrapper syntax appearing in logs | Deutet auf Wrapper-Proliferation im gesamten Codebase hin |
Fehlercode-Referenz
| Code | Bedeutung | Korrektur-Relevanz |
|---|---|---|
DISCORD_TRANSPORT_001 | Nachricht überschreitet 2000-Zeichen-Limit | Sanitisierung sollte kürzen, nicht fehlschlagen |
DISCORD_TRANSPORT_002 | Sanitisierungsfehler bei ausgehender Nachricht | Hard Block deutet auf ernsthaftes Leck hin |
CONTENT_SANITIZE_001 | Mustervergleich bei Input fehlgeschlagen | Regex-Schwachstelle ermöglicht Umgehung |
ATTACHMENT_EXTRACT_001 | Binäre Extraktion produzierte Nicht-Text | Korrupte Payload verwerfen, nicht weiterleiten |
ASYNC_COMPLETION_001 | Schmutzige Payload in Warteschlange erkannt | Pre-Delivery-Sanitisierung fehlt |
Zugehörige Konfigurationsparameter
| Parameter | Ort | Standard | Sicherheitsauswirkung |
|---|---|---|---|
SANITIZATION_ENABLED | Environment | true | Wenn false, wird alle Sanitisierung umgangen |
DISCORD_STRICT_MODE | Config | false | Wenn true, wird Hard Block bei Wrapper-Erkennung aktiviert |
ATTACHMENT_MAX_EXTRACT_CHARS | Config | 4000 | Verhindert Spam aus überdimensionierten Extraktionen |
ASYNC_COMPLETION_SANITIZE | Config | true | Muss für async-Pfad aktiviert bleiben |