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
| Componente | Manifestación |
|---|---|
| Transporte Discord | Las etiquetas de contenedor sin procesar aparecen en las cargas útiles de mensajes salientes |
| Manejador de archivos adjuntos | Resultados de extracción corruptos reenviados al canal |
| Completación de herramienta asíncrona | El texto de completación en cola incluye marcadores internos |
| Capa de saneamiento | Fallo 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:
- Consumirse internamente durante el ensamblaje del contexto
- Nunca serializarse a las capas de transporte salientes
- 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:
- Se procesan datos binarios o corruptos del archivo adjunto
- La extracción produce secuencias de Unicode/puntos de código corruptas
- Estas secuencias se repiten durante el manejo de múltiples archivos adjuntos
- La carga útil corrupta evade el filtrado de contenido
Responsabilidades de los subsistemas
| Subsistema | Comportamiento esperado | Comportamiento real |
|---|---|---|
DiscordTransport | Eliminar contenedores internos antes de publicar | Reenvía contenido sin procesar |
ContentSanitizer | Eliminar marcadores EXTERNAL_* | Filtro deshabilitado o evadido |
AttachmentHandler | Texto de extracción limpio | Pasa carga útil corrupta |
AsyncCompletionRouter | Entrega completación limpia | Incluye 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_CONTENTen el historial de mensajes de Discord - No hay cadenas
<<<END_EXTERNAL_UNTRUSTED_CONTENTen el historial de mensajes de Discord - No aparecen
Source: External/Source: Internalen 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/Problema | Descripción | Conexión |
|---|---|---|
Fuga del contenedor EXTERNAL_UNTRUSTED_CONTENT | Marcadores internos sin procesar visibles para los usuarios | Problema principal - síntoma idéntico |
| Corrupción de extracción de texto de archivos adjuntos | Texto basura/malformado de archivos adjuntos | Misma causa raíz: límite de saneamiento faltante |
| Spam de completación de herramientas asíncronas | Completaciones duplicadas/rotas en canales | Comparte el defecto de renderizado del transporte |
| Errores de límite de tasa de Discord | Puede ocurrir si la fuga causa un bucle de spam de mensajes | Síntoma secundario del contenido basura |
| Respaldo de cola de mensajes | Si el transporte falla repetidamente con contenido sucio | Consecuencia aguas abajo de entrada sin saneamiento |
Problemas históricamente relacionados
| ID del problema | Título | Relevancia |
|---|---|---|
| GH-XXX | Sanitizer not applied to async completion payloads | Predecesor directo - corrección no propagada a todas las rutas |
| GH-YYY | Discord transport bypasses content filtering in dev mode | Variante específica del entorno del fallo de límite |
| GH-ZZZ | Attachment extraction returning binary garbage | Mismo mecanismo de corrupción, subsistema diferente |
| GH-AAA | Internal wrapper syntax appearing in logs | Indica proliferación de contenedores en toda la base de código |
Referencia de códigos de error
| Código | Significado | Relevancia de la corrección |
|---|---|---|
DISCORD_TRANSPORT_001 | El mensaje excede el límite de 2000 caracteres | El saneamiento debe truncar, no fallar |
DISCORD_TRANSPORT_002 | Fallo de saneamiento en mensaje saliente | El bloqueo severo indica una fuga grave |
CONTENT_SANITIZE_001 | La coincidencia de patrón falló en la entrada | Vulnerabilidad de regex permite evasión |
ATTACHMENT_EXTRACT_001 | La extracción binaria produjo no-texto | Descartar carga útil corrupta, no reenviar |
ASYNC_COMPLETION_001 | Carga útil sucia detectada en cola | Falta saneamiento pre-entrega |
Parámetros de configuración relacionados
| Parámetro | Ubicación | Por defecto | Impacto de seguridad |
|---|---|---|---|
SANITIZATION_ENABLED | Entorno | true | Si es false, todo saneamiento evadido |
DISCORD_STRICT_MODE | Configuración | false | Si es true, habilita bloqueo severo en detección de contenedores |
ATTACHMENT_MAX_EXTRACT_CHARS | Configuración | 4000 | Previene spam de extracciones sobredimensionadas |
ASYNC_COMPLETION_SANITIZE | Configuración | true | Debe permanecer habilitado para la ruta asíncrona |