[Fallo de envío saliente de WhatsApp con política de lista blanca] - WhatsApp Outbound Send Failure with Allowlist Policy
Al usar dmPolicy 'allowlist' de WhatsApp, los mensajes salientes fallan con 'Delivering to WhatsApp requires target' porque los objetivos de sendTo deben estar en la misma lista allowFrom que controla el acceso entrante.
🔍 Síntomas
Manifestación principal del error
Al intentar enviar un mensaje de WhatsApp usando la herramienta message a un contacto que no está en la lista allowFrom:
Error: Delivering to WhatsApp requires target <E.164|group JID>
at resolveOutboundTarget (src/whatsapp/resolve-outbound-target.ts:XX)
at sendWhatsAppMessage (src/whatsapp/sender.ts:XX)
Contexto de la configuración
El problema se manifiesta cuando está configurada la siguiente configuración:
json { “channels”: { “whatsapp”: { “dmPolicy”: “allowlist”, “allowFrom”: ["+1234567890"] } } }
Comandos de diagnóstico de CLI
bash
Attempt to send a message to an unlisted contact
$ openclaw tools call message ‘{“to”: “+0987654321”, “body”: “Hello”}’
Expected: Message sent successfully
Actual: Error - Delivering to WhatsApp requires target <E.164|group JID>
Síntoma secundario: Modelo de seguridad confuso
Los usuarios observan que agregar un contacto a allowFrom tiene efectos duales:
- El contacto ahora puede recibir mensajes salientes del bot
- El contacto también puede enviar mensajes entrantes que activen el bot
Esto viola el principio de mínimo privilegio y crea confusión de seguridad.
🧠 Causa raíz
Análisis arquitectónico
La causa raíz radica en la dependencia de datos compartida entre la lógica de control de acceso entrante y saliente.
Archivo: src/whatsapp/resolve-outbound-target.ts
typescript
export async function resolveOutboundTarget(
normalizedTo: string,
allowList: string[]
): Promise
const hasWildcard = allowList.includes("*");
if (hasWildcard || allowList.length === 0) { return { ok: true, to: normalizedTo }; }
if (allowList.includes(normalizedTo)) { return { ok: true, to: normalizedTo }; }
return {
ok: false,
error: Delivering to WhatsApp requires target <E.164|group JID>,
};
}
El problema: Esta función recibe el array allowFrom como parámetro allowList, lo que significa que el permiso de salida está controlado por la configuración de entrada.
Archivo: src/web/inbound/access-control.ts
typescript export function checkInboundAccess( from: string, allowFrom: string[] ): InboundAccessResult { const hasWildcard = allowFrom.includes("*"); const isAllowed = hasWildcard || allowFrom.includes(from);
return { allowed: isAllowed, reason: isAllowed ? “allowed” : “inbound_not_authorized” }; }
El problema del punto de control compartido
| Configuración | Efecto entrante | Efecto saliente |
|---|---|---|
"allowFrom": ["+1234567890"] | Solo +1234567890 puede activar el bot | El bot solo puede enviar a +1234567890 |
"allowFrom": ["*"] | Cualquiera puede activar el bot | El bot puede enviar a cualquiera |
Violación del diseño
La implementación actual viola el principio de separación de responsabilidades. El campo allowFrom fue diseñado para el control de acceso entrante pero se reutiliza para la autorización saliente, creando un acoplamiento no intencionado.
🛠️ Solución paso a paso
Fase 1: Agregar tipo de configuración
Archivo: src/config/types.whatsapp.ts
Antes: typescript export interface WhatsAppConfig { dmPolicy: “allowlist” | “open”; allowFrom: string[]; // … other fields }
Después: typescript export interface WhatsAppConfig { dmPolicy: “allowlist” | “open”; allowFrom: string[]; allowSendTo?: string[]; // NUEVO: Lista de permitidos separada para salida // … other fields }
Fase 2: Actualizar la lógica de resolución de salida
Archivo: src/whatsapp/resolve-outbound-target.ts
Antes:
typescript
export async function resolveOutboundTarget(
normalizedTo: string,
allowList: string[]
): Promise
const hasWildcard = allowList.includes("*");
if (hasWildcard || allowList.length === 0) { return { ok: true, to: normalizedTo }; }
if (allowList.includes(normalizedTo)) { return { ok: true, to: normalizedTo }; }
return {
ok: false,
error: Delivering to WhatsApp requires target <E.164|group JID>,
};
}
Después:
typescript
export async function resolveOutboundTarget(
normalizedTo: string,
sendToList: string[] | undefined,
inboundAllowFrom: string[]
): Promise
// Si sendTo está explícitamente configurado, usarlo if (sendToList !== undefined) { const hasWildcard = sendToList.includes("*");
if (hasWildcard || sendToList.length === 0) {
return { ok: true, to: normalizedTo };
}
if (sendToList.includes(normalizedTo)) {
return { ok: true, to: normalizedTo };
}
return {
ok: false,
error: `Target ${normalizedTo} is not in allowSendTo list`,
};
}
// Comportamiento heredado (usar allowFrom entrante para salida) const hasWildcard = inboundAllowFrom.includes("*");
if (hasWildcard || inboundAllowFrom.length === 0) { return { ok: true, to: normalizedTo }; }
if (inboundAllowFrom.includes(normalizedTo)) { return { ok: true, to: normalizedTo }; }
return {
ok: false,
error: Delivering to WhatsApp requires target <E.164|group JID>,
};
}
Fase 3: Actualizar los sitios de llamada
Archivo: src/whatsapp/sender.ts (o donde sea que se llame resolveOutboundTarget)
Antes: typescript const target = await resolveOutboundTarget( normalizedTo, config.allowFrom // Pasando lista entrante para verificación de salida );
Después: typescript const target = await resolveOutboundTarget( normalizedTo, config.allowSendTo, // Usar lista dedicada para salida config.allowFrom // Pasar para compatibilidad heredada );
Fase 4: Ejemplo de configuración
Configuración recomendada para producción:
json { “channels”: { “whatsapp”: { “dmPolicy”: “allowlist”, “allowFrom”: ["+1234567890", “+1111111111”], “allowSendTo”: ["*"] } } }
Configuración estricta de salida:
json { “channels”: { “whatsapp”: { “dmPolicy”: “allowlist”, “allowFrom”: ["+1234567890"], “allowSendTo”: [ “+0987654321”, “+1122334455”, “12036301234567890-1234567890@g.us” ] } } }
🧪 Verificación
Caso de prueba 1: Salida a SendTo en lista de permitidos
bash
Configuration
“allowSendTo”: ["+0987654321"]
$ openclaw tools call message ‘{“to”: “+0987654321”, “body”: “Test”}’
Salida esperada: json { “ok”: true, “messageId”: “wamid.xxx…”, “timestamp”: “2024-01-15T10:30:00Z” }
Caso de prueba 2: Salida a SendTo no listado
bash
Configuration
“allowSendTo”: ["+0987654321"]
$ openclaw tools call message ‘{“to”: “+5555555555”, “body”: “Test”}’
Salida esperada: json { “ok”: false, “error”: “Target +5555555555 is not in allowSendTo list” }
Caso de prueba 3: Entrada desde remitente en lista de permitidos
bash
Configuration
“allowFrom”: ["+0987654321"]
“allowSendTo”: ["*"]
Send message FROM +0987654321 TO the bot
Comportamiento esperado: El mensaje es procesado y activa la respuesta del bot.
Caso de prueba 4: Entrada desde remitente no listado
bash
Configuration
“allowFrom”: ["+0987654321"]
Send message FROM +5555555555 TO the bot
Comportamiento esperado: El mensaje es rechazado con un error de control de acceso entrante.
Caso de prueba 5: SendTo con comodín
bash
Configuration
“allowSendTo”: ["*"]
$ openclaw tools call message ‘{“to”: “+anyvalidnumber”, “body”: “Test”}’
Salida esperada: El mensaje se envía exitosamente.
Script de verificación
typescript // test/whatsapp-outbound-permissions.test.ts
import { resolveOutboundTarget } from “../src/whatsapp/resolve-outbound-target”;
describe(“resolveOutboundTarget”, () => { test(“allows when target is in sendTo list”, async () => { const result = await resolveOutboundTarget( “+0987654321”, ["+0987654321", “+1122334455”], ["+1234567890"] ); expect(result.ok).toBe(true); });
test(“blocks when target is not in sendTo list”, async () => { const result = await resolveOutboundTarget( “+5555555555”, ["+0987654321"], ["+1234567890"] ); expect(result.ok).toBe(false); expect(result.error).toContain(“not in allowSendTo list”); });
test(“allows wildcard sendTo”, async () => { const result = await resolveOutboundTarget( “+5555555555”, ["*"], ["+1234567890"] ); expect(result.ok).toBe(true); });
test(“falls back to allowFrom when sendTo is undefined”, async () => { const result = await resolveOutboundTarget( “+1234567890”, undefined, // sendTo not configured ["+1234567890"] ); expect(result.ok).toBe(true); }); });
⚠️ Errores comunes
Error 1: Formato E.164 incorrecto
WhatsApp requiere números en formato E.164 (por ejemplo, +1234567890). Usar formatos sin el + inicial causará fallos silenciosos.
bash
INCORRECTO
$ openclaw tools call message ‘{“to”: “1234567890”, “body”: “Test”}’
CORRECTO
$ openclaw tools call message ‘{“to”: “+1234567890”, “body”: “Test”}’
Error 2: JID de grupo vs. Número de teléfono
Los IDs de grupo usan un formato diferente a los números de teléfono. Asegúrate de usar la sintaxis correcta de JID:
json { “allowSendTo”: [ “+1234567890”, // Número de teléfono “12036301234567890-1234567890@g.us” // JID de grupo ] }
Error 3: Array vacío vs. Indefinido
Un array allowSendTo: [] vacío es tratado de manera diferente a que allowSendTo esté indefinido:
"allowSendTo": []— Bloquea todos los mensajes salientes"allowSendTo": undefined— Vuelve al comportamiento heredado de allowFrom
Error 4: Mapeo de variables de entorno en Docker
Al usar variables de entorno para la configuración:
bash
INCORRECTO - Esto crea un string, no un array
WHATSAPP_ALLOW_SEND_TO=+1234567890,+0987654321
CORRECTO - Usar string JSON para arrays
WHATSAPP_ALLOW_SEND_TO=["+1234567890","+0987654321"]
Error 5: Problemas de caché
Después de actualizar la configuración, asegúrate de que el proceso en ejecución recargue:
bash
Reiniciar el servicio OpenClaw
$ systemctl restart openclaw
O para Docker
$ docker-compose down && docker-compose up -d
Error 6: Migración desde configuración heredada
Las configuraciones existentes sin allowSendTo deberían seguir funcionando mediante el mecanismo de respaldo. Sin embargo, prueba exhaustivamente:
typescript // Verificar que el fallback aún funciona const sendTo = config.allowSendTo ?? config.allowFrom;
Error 7: Implicaciones de seguridad del comodín
Establecer "allowSendTo": ["*"] permite enviar a cualquier número de WhatsApp válido. Considera:
- Limitación de tasa en la herramienta de mensajes
- Autorización adicional a nivel de aplicación
- Registro de todos los intentos de mensajes salientes
🔗 Errores relacionados
E_DELIVERY_FAILED— Fallo de entrega genérico cuando la API de WhatsApp rechaza el mensajeE_INVALID_TARGET— El formato del número objetivo es inválido (no cumple con E.164)E_INBOUND_NOT_AUTHORIZED— Mensaje entrante rechazado debido a la política de lista de permitidosE_SESSION_NOT_READY— La sesión de WhatsApp no está establecida antes del intento de salidaE_ALLOWLIST_BLOCKED— El objetivo saliente no está en la lista de permitidos configurada
Issues de GitHub relacionados
- Issue #XXX — La política allowlist de dmPolicy de WhatsApp bloquea mensajes salientes legítimos
- Issue #YYY — Solicitud: Separar el control de acceso entrante/saliente para WhatsApp
- Issue #ZZZ — Documentación: El modelo de seguridad del canal de WhatsApp no está claro
Referencia de configuración
channels.whatsapp.dmPolicy— Controla el modo de acceso entrante (`"allowlist"` | `"open"`)channels.whatsapp.allowFrom— Lista de permitidos entrante (números de teléfono y JIDs de grupo)channels.whatsapp.allowSendTo— Lista de permitidos saliente (números de teléfono y JIDs de grupo) [NUEVO]