April 17, 2026

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

ComponentManifestation
Discord TransportRaw wrapper tags appear in outbound message payloads
Attachment HandlerCorrupted extraction results forwarded to channel
Async Tool CompletionQueued completion text includes internal markers
Sanitization LayerBoundary 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_CONTENT wrapper 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:

  1. Consumed internally during context assembly
  2. Never serialized to outbound transport layers
  3. 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:

  1. Binary or malformed attachment data is processed
  2. Extraction produces corrupted Unicode/code-point sequences
  3. These sequences are repeated during multi-attachment handling
  4. The corrupted payload bypasses content filtering

Subsystem Responsibilities

SubsystemExpected BehaviorActual Behavior
DiscordTransportStrip internal wrappers before postingForwards raw content
ContentSanitizerRemove EXTERNAL_* markersFilter disabled or bypassed
AttachmentHandlerClean extraction textPasses corrupted payload
AsyncCompletionRouterDelivers clean completionIncludes 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_CONTENT strings in Discord message history
  • No <<<END_EXTERNAL_UNTRUSTED_CONTENT strings in Discord message history
  • No Source: External / Source: Internal appearing in user-visible messages
  • Attachment-extracted text contains no repetitive token patterns (<20% unique ratio)
  • Unit tests pass for sanitizeForDiscord function
  • 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
}
Error/IssueDescriptionConnection
EXTERNAL_UNTRUSTED_CONTENT wrapper leakRaw internal markers visible to usersPrimary issue - identical symptom
Attachment text extraction corruptionGarbage/malformed text from attachmentsSame root cause: missing sanitization boundary
Async tool completion spamDuplicate/broken completions in channelsShares transport rendering defect
Discord rate limit errorsMay occur if leak causes message spam loopSecondary symptom from garbage content
Message queue backupIf transport repeatedly fails on dirty contentDownstream consequence of unsanitized input
Issue IDTitleRelevance
GH-XXXSanitizer not applied to async completion payloadsDirect predecessor - fix not propagated to all paths
GH-YYYDiscord transport bypasses content filtering in dev modeEnvironment-specific variant of boundary failure
GH-ZZZAttachment extraction returning binary garbageSame corruption mechanism, different subsystem
GH-AAAInternal wrapper syntax appearing in logsIndicates wrapper proliferation across codebase

Error Code Reference

CodeMeaningFix Relevance
DISCORD_TRANSPORT_001Message exceeds 2000 character limitSanitization should truncate, not fail
DISCORD_TRANSPORT_002Sanitization failure on outbound messageHard block indicates serious leak
CONTENT_SANITIZE_001Pattern match failed on inputRegex vulnerability allows bypass
ATTACHMENT_EXTRACT_001Binary extraction produced non-textDiscard corrupted payload, do not forward
ASYNC_COMPLETION_001Dirty payload detected in queuePre-delivery sanitization missing
ParameterLocationDefaultSecurity Impact
SANITIZATION_ENABLEDEnvironmenttrueIf false, all sanitization bypassed
DISCORD_STRICT_MODEConfigfalseIf true, enables hard block on wrapper detection
ATTACHMENT_MAX_EXTRACT_CHARSConfig4000Prevents spam from oversized extractions
ASYNC_COMPLETION_SANITIZEConfigtrueMust remain enabled for async path

Evidence & Sources

This troubleshooting guide was automatically synthesized by the FixClaw Intelligence Pipeline from community discussions.