Bypass de llamadas fail-closed (review de seguridad)

El gate del allowlist miraba allow.length>0 (tras parsear), así que un typo en
RETELL_ALLOWED_NUMBERS que no dejara ningún número válido abría las llamadas a
todos (fail-open). Ahora mira la presencia de la variable: si está puesta, se
enforce; un typo restringe a nadie (fail-closed), acorde a la intención.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Carlos Narro
2026-06-07 19:04:27 +02:00
parent db46dc9cf3
commit 98f02eb02e

View File

@@ -53,12 +53,15 @@ export async function iniciarLlamadaSaliente(opts: {
// Bypass de seguridad (fase de pruebas): si RETELL_ALLOWED_NUMBERS tiene valor, SOLO se llama a // Bypass de seguridad (fase de pruebas): si RETELL_ALLOWED_NUMBERS tiene valor, SOLO se llama a
// esos números; cualquier otro se omite (devuelve null, el funnel sigue en simulado). Vaciar la // esos números; cualquier otro se omite (devuelve null, el funnel sigue en simulado). Vaciar la
// variable para volver a llamar a todos. // variable para volver a llamar a todos.
// FAIL-CLOSED: el gate mira la PRESENCIA de la variable, no si parsea. Así un typo que no deje
// ningún número válido restringe a nadie (no llama), en vez de abrirse a todos.
const allowConfigurado = (env.RETELL_ALLOWED_NUMBERS ?? '').trim().length > 0;
const allow = (env.RETELL_ALLOWED_NUMBERS ?? '') const allow = (env.RETELL_ALLOWED_NUMBERS ?? '')
.split(',') .split(',')
.map((s) => normalizarTelefonoEs(s.trim())) .map((s) => normalizarTelefonoEs(s.trim()))
.filter((n): n is string => Boolean(n)); .filter((n): n is string => Boolean(n));
if (allow.length > 0 && !allow.includes(toNumber)) { if (allowConfigurado && !allow.includes(toNumber)) {
console.warn(`Retell: ${toNumber} no está en RETELL_ALLOWED_NUMBERS; llamada omitida (bypass).`); console.warn(`Retell: ${toNumber} no permitido por RETELL_ALLOWED_NUMBERS; llamada omitida (bypass).`);
return null; return null;
} }