Discord Channel Leaking Internal Payloads: EXTERNAL_UNTRUSTED_CONTENT Wrappers in User Messages
Internal wrapper markers and malformed attachment extraction text are being forwarded to Discord channels instead of being sanitized before transmission.
๐ Symptoms
Observed User-Facing Errors
When interacting with the assistant via Discord, users observe messages containing raw internal content that should never reach the presentation layer. The leaked content manifests in two distinct patterns:
Pattern 1: Wrapper Syntax Leakage
Messages containing raw serialization markers appear directly in Discord chat:
<<<EXTERNAL_UNTRUSTED_CONTENT id="msg_abc123">>>
Source: External
UNTRUSTED Discord message body
<<<END_EXTERNAL_UNTRUSTED_CONTENT id="msg_abc123">>>
Pattern 2: Malformed Attachment Payload Spam
Large blocks of nonsensical text dominated by repeated technical terms:
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
Technical Manifestations
| Component | Manifestation |
|---|---|
| Discord Transport | Raw wrapper tags appear in outbound message payloads |
| Attachment Handler | Corrupted extraction results forwarded to channel |
| Async Tool Completion | Queued completion text includes internal markers |
| Sanitization Layer | Boundary enforcement failure between context and render |
Trigger Conditions
The issue occurs after any of the following operations:
- Assistant processes a message containing attachments
- Async tool completion delivers results to Discord channel
- External content is processed through the
EXTERNAL_UNTRUSTED_CONTENTwrapper system - Multi-turn conversation involves file/image attachments
๐ง Root Cause
Architectural Failure Points
The leakage indicates a sanitization boundary failure in the message pipeline between internal processing and Discord transport. The OpenClaw framework uses the EXTERNAL_UNTRUSTED_CONTENT wrapper to isolate untrusted user content during agent processing. This wrapper should be:
- Consumed internally during context assembly
- Never serialized to outbound transport layers
- Stripped before any message reaches the rendering pipeline
Failure Sequence
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ 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 Path Analysis
The defect exists in the Discord transport adapter where the response message is constructed. The expected code path:
// 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;
}
Attachment Payload Corruption
The “garbage text” pattern results from attachment text extraction where:
- Binary or malformed attachment data is processed
- Extraction produces corrupted Unicode/code-point sequences
- These sequences are repeated during multi-attachment handling
- The corrupted payload bypasses content filtering
Subsystem Responsibilities
| Subsystem | Expected Behavior | Actual Behavior |
|---|---|---|
DiscordTransport | Strip internal wrappers before posting | Forwards raw content |
ContentSanitizer | Remove EXTERNAL_* markers | Filter disabled or bypassed |
AttachmentHandler | Clean extraction text | Passes corrupted payload |
AsyncCompletionRouter | Delivers clean completion | Includes debug markers |
๐ ๏ธ Step-by-Step Fix
Phase 1: Disable Wrapper Propagation in Discord Transport
File: src/transports/discord/index.ts (or equivalent transport module)
Before (Defective):
async function handleAssistantMessage(message: ProcessedMessage): Promise<void> {
const discordMessage = {
content: message.content,
embeds: message.embeds
};
await this.client.sendMessage(discordMessage);
}
After (Fixed):
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: Strengthen Attachment Extraction Sanitization
File: src/handlers/attachment-extractor.ts
Before (Defective):
function extractTextFromAttachment(attachment: Attachment): string {
const raw = processAttachmentBinary(attachment);
return raw.text || '';
}
After (Fixed):
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: Fix Async Tool Completion Routing
File: src/routing/async-completion-router.ts
Before (Defective):
async function forwardCompletion(result: ToolResult): Promise<void> {
const message = buildChannelMessage(result);
await this.transport.post(message);
}
After (Fixed):
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: Add Transport-Level Guard
File: src/transports/discord/client.ts
Add a final sanitization gate before any Discord API call:
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);
}
๐งช Verification
Test Case 1: Wrapper Marker Stripping
Execute the sanitization function against known-internal content:
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);
Expected Output:
โ
PASS: "UNTRUSTED Discord message body"
โ
PASS: "User message"
โ
PASS: "Valid response"
Results: 3/3 tests passed
Test Case 2: End-to-End 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;
}
}
Test Case 3: Malformed Attachment Detection
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
Expected Output:
Corrupted detection: true
Valid detection: false
Verification Checklist
After applying fixes, confirm:
- No
<<<EXTERNAL_UNTRUSTED_CONTENTstrings in Discord message history - No
<<<END_EXTERNAL_UNTRUSTED_CONTENTstrings in Discord message history - No
Source: External/Source: Internalappearing in user-visible messages - Attachment-extracted text contains no repetitive token patterns (<20% unique ratio)
- Unit tests pass for
sanitizeForDiscordfunction - Integration tests pass for Discord transport
- Hard block throws error if wrapper syntax detected at send time
โ ๏ธ Common Pitfalls
Environment-Specific Traps
Docker Container Isolation
If running OpenClaw in Docker, ensure the sanitization module is properly mounted and not overridden by a volume that reverts to the buggy version:
# 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 Line Endings
The wrapper regex may fail if content contains \r\n line endings. Ensure sanitization handles both:
// 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 Version Incompatibilities
The Set constructor for unique ratio calculation requires Node.js 12+. Verify compatibility:
// Feature detection fallback
const uniqueRatio = typeof Set !== 'undefined'
? new Set(tokens).size / tokens.length
: [...new Set(tokens)].length / tokens.length;
Configuration Pitfalls
Sanitization Disabled by Environment Variable
Some deployments disable sanitization for debugging, which will cause this leak:
# .env file - ensure sanitization is NOT disabled
SANITIZATION_ENABLED=true
# SANITIZATION_ENABLED=false โ REMOVE OR SET TO TRUE
Transport Config Not Inheriting Base Sanitizer
If using a custom Discord transport implementation, ensure it inherits the base ContentSanitizer:
// 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 Normalization Attacks
Malicious content may use Unicode lookalike characters to bypass pattern matching:
// 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);
Concurrent Message Sanitization Race Condition
If multiple async tool completions fire simultaneously:
// 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, '');
}
Empty Sanitization Result
If sanitization strips all content, ensure the message is not sent (avoids empty spam):
const sanitized = stripInternalMarkers(raw);
if (!sanitized.trim()) {
logger.warn('[Discord] Sanitization produced empty message, discarding');
return; // Do not post to Discord
}
๐ Related Errors
Directly Related Issues
| Error/Issue | Description | Connection |
|---|---|---|
EXTERNAL_UNTRUSTED_CONTENT wrapper leak | Raw internal markers visible to users | Primary issue - identical symptom |
| Attachment text extraction corruption | Garbage/malformed text from attachments | Same root cause: missing sanitization boundary |
| Async tool completion spam | Duplicate/broken completions in channels | Shares transport rendering defect |
| Discord rate limit errors | May occur if leak causes message spam loop | Secondary symptom from garbage content |
| Message queue backup | If transport repeatedly fails on dirty content | Downstream consequence of unsanitized input |
Historically Related Issues
| Issue ID | Title | Relevance |
|---|---|---|
| GH-XXX | Sanitizer not applied to async completion payloads | Direct predecessor - fix not propagated to all paths |
| GH-YYY | Discord transport bypasses content filtering in dev mode | Environment-specific variant of boundary failure |
| GH-ZZZ | Attachment extraction returning binary garbage | Same corruption mechanism, different subsystem |
| GH-AAA | Internal wrapper syntax appearing in logs | Indicates wrapper proliferation across codebase |
Error Code Reference
| Code | Meaning | Fix Relevance |
|---|---|---|
DISCORD_TRANSPORT_001 | Message exceeds 2000 character limit | Sanitization should truncate, not fail |
DISCORD_TRANSPORT_002 | Sanitization failure on outbound message | Hard block indicates serious leak |
CONTENT_SANITIZE_001 | Pattern match failed on input | Regex vulnerability allows bypass |
ATTACHMENT_EXTRACT_001 | Binary extraction produced non-text | Discard corrupted payload, do not forward |
ASYNC_COMPLETION_001 | Dirty payload detected in queue | Pre-delivery sanitization missing |
Related Configuration Parameters
| Parameter | Location | Default | Security Impact |
|---|---|---|---|
SANITIZATION_ENABLED | Environment | true | If false, all sanitization bypassed |
DISCORD_STRICT_MODE | Config | false | If true, enables hard block on wrapper detection |
ATTACHMENT_MAX_EXTRACT_CHARS | Config | 4000 | Prevents spam from oversized extractions |
ASYNC_COMPLETION_SANITIZE | Config | true | Must remain enabled for async path |