April 17, 2026

Canal de Discord Filtrando Cargas Útiles Internas: Envolturas EXTERNAL_UNTRUSTED_CONTENT en Mensajes de Usuario

Los marcadores de envoltura internos y el texto de extracción de archivos adjuntos mal formados están siendo reenviados a canales de Discord en lugar de ser saneados antes de la transmisión.

🔍 Síntomas

Errores observados por el usuario

Al interactuar con el asistente a través de Discord, los usuarios observan mensajes que contienen contenido interno sin procesar que nunca debería llegar a la capa de presentación. El contenido filtrado se manifiesta en dos patrones distintos:

Patrón 1: Fuga de sintaxis de contenedores

Mensajes que contienen marcadores de serialización sin procesar aparecen directamente en el chat de Discord:

<<<EXTERNAL_UNTRUSTED_CONTENT id="msg_abc123">>>
Source: External
UNTRUSTED Discord message body
<<<END_EXTERNAL_UNTRUSTED_CONTENT id="msg_abc123">>>

Patrón 2: Spam de carga útil de archivos adjuntos corrupta

Bloques grandes de texto sin sentido dominados por términos técnicos repetidos:

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

Manifestaciones técnicas

ComponenteManifestación
Transporte DiscordLas etiquetas de contenedor sin procesar aparecen en las cargas útiles de mensajes salientes
Manejador de archivos adjuntosResultados de extracción corruptos reenviados al canal
Completación de herramienta asíncronaEl texto de completación en cola incluye marcadores internos
Capa de saneamientoFallo en el cumplimiento de límites entre contexto y renderizado

Condiciones de activación

El problema ocurre después de cualquiera de las siguientes operaciones:

  • El asistente procesa un mensaje que contiene archivos adjuntos
  • La completación de herramienta asíncrona entrega resultados al canal de Discord
  • El contenido externo se procesa a través del sistema de contenedor EXTERNAL_UNTRUSTED_CONTENT
  • La conversación de múltiples turnos involucra archivos adjuntos de archivos/imágenes

🧠 Causa raíz

Puntos de fallo arquitectónicos

La fuga indica un fallo en el límite de saneamiento en la tubería de mensajes entre el procesamiento interno y el transporte a Discord. El framework OpenClaw utiliza el contenedor EXTERNAL_UNTRUSTED_CONTENT para aislar el contenido no confiable del usuario durante el procesamiento del agente. Este contenedor debería:

  1. Consumirse internamente durante el ensamblaje del contexto
  2. Nunca serializarse a las capas de transporte salientes
  3. Eliminarse antes de que cualquier mensaje llegue a la tubería de renderizado

Secuencia de fallo

┌─────────────────────────────────────────────────────────────────┐
│                    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          │
│  └─────────────────┘                                            │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

Análisis de la ruta del código

El defecto existe en el adaptador de transporte de Discord donde se construye el mensaje de respuesta. La ruta del código esperada:

// 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;
}

Corrupción de la carga útil de archivos adjuntos

El patrón de “texto basura” resulta de la extracción de texto de archivos adjuntos donde:

  1. Se procesan datos binarios o corruptos del archivo adjunto
  2. La extracción produce secuencias de Unicode/puntos de código corruptas
  3. Estas secuencias se repiten durante el manejo de múltiples archivos adjuntos
  4. La carga útil corrupta evade el filtrado de contenido

Responsabilidades de los subsistemas

SubsistemaComportamiento esperadoComportamiento real
DiscordTransportEliminar contenedores internos antes de publicarReenvía contenido sin procesar
ContentSanitizerEliminar marcadores EXTERNAL_*Filtro deshabilitado o evadido
AttachmentHandlerTexto de extracción limpioPasa carga útil corrupta
AsyncCompletionRouterEntrega completación limpiaIncluye marcadores de depuración

🛠️ Solución paso a paso

Fase 1: Deshabilitar la propagación del contenedor en el transporte Discord

Archivo: src/transports/discord/index.ts (o módulo de transporte equivalente)

Antes (Defectuoso):

async function handleAssistantMessage(message: ProcessedMessage): Promise<void> {
    const discordMessage = {
        content: message.content,
        embeds: message.embeds
    };
    await this.client.sendMessage(discordMessage);
}

Después (Corregido):

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();
}

Fase 2: Fortalecer el saneamiento de extracción de archivos adjuntos

Archivo: src/handlers/attachment-extractor.ts

Antes (Defectuoso):

function extractTextFromAttachment(attachment: Attachment): string {
    const raw = processAttachmentBinary(attachment);
    return raw.text || '';
}

Después (Corregido):

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;
}

Fase 3: Corregir el enrutamiento de completación de herramientas asíncronas

Archivo: src/routing/async-completion-router.ts

Antes (Defectuoso):

async function forwardCompletion(result: ToolResult): Promise<void> {
    const message = buildChannelMessage(result);
    await this.transport.post(message);
}

Después (Corregido):

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);
}

Fase 4: Agregar protección a nivel de transporte

Archivo: src/transports/discord/client.ts

Agregar una validación final de saneamiento antes de cualquier llamada a la API de Discord:

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('SANITIZATION_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);
}

🧪 Verificación

Caso de prueba 1: Eliminación de marcadores de contenedor

Ejecutar la función de saneamiento contra contenido interno conocido:

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);

Salida esperada:

✅ PASS: "UNTRUSTED Discord message body"
✅ PASS: "User message"
✅ PASS: "Valid response"

Results: 3/3 tests passed

Caso de prueba 2: Prueba de transporte Discord de extremo a extremo

// 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;
    }
}

Caso de prueba 3: Detección de extracción de archivos adjuntos corrupta

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

Salida esperada:

Corrupted detection: true
Valid detection: false

Lista de verificación de verificación

Después de aplicar las correcciones, confirme:

  • No hay cadenas <<<EXTERNAL_UNTRUSTED_CONTENT en el historial de mensajes de Discord
  • No hay cadenas <<<END_EXTERNAL_UNTRUSTED_CONTENT en el historial de mensajes de Discord
  • No aparecen Source: External / Source: Internal en mensajes visibles para el usuario
  • El texto extraído de archivos adjuntos no contiene patrones de tokens repetitivos (<20% de ratio único)
  • Las pruebas unitarias pasan para la función sanitizeForDiscord
  • Las pruebas de integración pasan para el transporte Discord
  • El bloqueo severo lanza error si se detecta sintaxis de contenedor en el momento del envío

⚠️ Errores comunes

Trampas específicas del entorno

Aislamiento de contenedores Docker

Si se ejecuta OpenClaw en Docker, asegúrese de que el módulo de saneamiento esté correctamente montado y no sea reemplazado por un volumen que revierta a la versión con errores:

# Wrong - local source overrides container
docker run -v $(pwd)/src:/app/src openclaw:latest

# Correct - use container's fixed source
docker run openclaw:latest

Finales de línea de Windows

La expresión regular del contenedor puede fallar si el contenido contiene finales de línea \r\n. Asegúrese de que el saneamiento maneje ambos:

// 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;

Incompatibilidades de versión de Node.js

El constructor Set para el cálculo del ratio único requiere Node.js 12+. Verifique la compatibilidad:

// Feature detection fallback
const uniqueRatio = typeof Set !== 'undefined' 
    ? new Set(tokens).size / tokens.length 
    : [...new Set(tokens)].length / tokens.length;

Errores de configuración

Saneamiento deshabilitado por variable de entorno

Algunos despliegues deshabilitan el saneamiento para depuración, lo que causará esta fuga:

# .env file - ensure sanitization is NOT disabled
SANITIZATION_ENABLED=true
# SANITIZATION_ENABLED=false  ← REMOVE OR SET TO TRUE

Configuración de transporte que no hereda el saneamiento base

Si usa una implementación de transporte Discord personalizada, asegúrese de que herede el ContentSanitizer base:

// 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));
    }
}

Casos extremos en tiempo de ejecución

Ataques de normalización Unicode

El contenido malicioso puede usar caracteres Unicode similares para evadir la coincidencia de patrones:

// 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);

Condición de carrera en saneamiento de mensajes concurrentes

Si múltiples completaciones de herramientas asíncronas se ejecutan simultáneamente:

// 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, '');
}

Resultado de saneamiento vacío

Si el saneamiento elimina todo el contenido, asegúrese de que el mensaje no se envíe (evita spam vacío):

const sanitized = stripInternalMarkers(raw);
if (!sanitized.trim()) {
    logger.warn('[Discord] Sanitization produced empty message, discarding');
    return; // Do not post to Discord
}

🔗 Errores relacionados

Problemas directamente relacionados

Error/ProblemaDescripciónConexión
Fuga del contenedor EXTERNAL_UNTRUSTED_CONTENTMarcadores internos sin procesar visibles para los usuariosProblema principal - síntoma idéntico
Corrupción de extracción de texto de archivos adjuntosTexto basura/malformado de archivos adjuntosMisma causa raíz: límite de saneamiento faltante
Spam de completación de herramientas asíncronasCompletaciones duplicadas/rotas en canalesComparte el defecto de renderizado del transporte
Errores de límite de tasa de DiscordPuede ocurrir si la fuga causa un bucle de spam de mensajesSíntoma secundario del contenido basura
Respaldo de cola de mensajesSi el transporte falla repetidamente con contenido sucioConsecuencia aguas abajo de entrada sin saneamiento

Problemas históricamente relacionados

ID del problemaTítuloRelevancia
GH-XXXSanitizer not applied to async completion payloadsPredecesor directo - corrección no propagada a todas las rutas
GH-YYYDiscord transport bypasses content filtering in dev modeVariante específica del entorno del fallo de límite
GH-ZZZAttachment extraction returning binary garbageMismo mecanismo de corrupción, subsistema diferente
GH-AAAInternal wrapper syntax appearing in logsIndica proliferación de contenedores en toda la base de código

Referencia de códigos de error

CódigoSignificadoRelevancia de la corrección
DISCORD_TRANSPORT_001El mensaje excede el límite de 2000 caracteresEl saneamiento debe truncar, no fallar
DISCORD_TRANSPORT_002Fallo de saneamiento en mensaje salienteEl bloqueo severo indica una fuga grave
CONTENT_SANITIZE_001La coincidencia de patrón falló en la entradaVulnerabilidad de regex permite evasión
ATTACHMENT_EXTRACT_001La extracción binaria produjo no-textoDescartar carga útil corrupta, no reenviar
ASYNC_COMPLETION_001Carga útil sucia detectada en colaFalta saneamiento pre-entrega

Parámetros de configuración relacionados

ParámetroUbicaciónPor defectoImpacto de seguridad
SANITIZATION_ENABLEDEntornotrueSi es false, todo saneamiento evadido
DISCORD_STRICT_MODEConfiguraciónfalseSi es true, habilita bloqueo severo en detección de contenedores
ATTACHMENT_MAX_EXTRACT_CHARSConfiguración4000Previene spam de extracciones sobredimensionadas
ASYNC_COMPLETION_SANITIZEConfiguracióntrueDebe permanecer habilitado para la ruta asíncrona

Evidencia y fuentes

Esta guía de solución de problemas fue sintetizada automáticamente por la tubería de inteligencia de FixClaw a partir de las discusiones de la comunidad.