Cheatsheet

Webhooks sin verificación de firma: de "falta un header" a fraude de integridad de datos

Nota de origen: cheatsheet basado en un script y hallazgo REALES de un engagement autorizado (plataforma de cobranza con agentes de voz IA integrados vía un proveedor externo). Anonimizado.

El hallazgo real

El endpoint POST /api/v1/webhooks/<proveedor> recibía eventos de fin de llamada de un proveedor de voz/IA externo (transcript, metadata, resultado del análisis de la conversación) y los procesaba sin verificar el header de firma HMAC que el proveedor sí envía (X-<Proveedor>-Signature). El propio research automatizado de Websec Argus sobre este proveedor (ver [[../Investigación/Hallazgos/2026-08-07-hallazgo-webhook-de-elevenlabs-sin-validacion-de-firma-exploit-public|hallazgo relacionado]]) confirmó que el SDK oficial SÍ ofrece verificación de firma lista para usar — el gap no está en el proveedor, está en que la implementación del CLIENTE nunca llamó a esa verificación.

Por qué importa más que "falta un header"

No es un simple hallazgo de "falta autenticación" — el CONTENIDO del evento falsificado altera datos de negocio con impacto financiero directo: en una plataforma de cobranza, el campo analysis.evaluation_criteria_results de un evento de llamada alimenta directamente si se registra una "promesa de pago" del deudor. Un evento falsificado puede:

  • Crear promesas de pago falsas (o borrar promesas reales sobreescribiendo el registro real con uno falso para el mismo conversation_id).
  • Inundar la cola de procesamiento de eventos (--count 500+) — DoS del pipeline de webhooks.

Script real (patrón generalizado)

EVENT = {
    "conversation_id": "<ID-REAL-DE-UNA-LLAMADA-EN-CURSO>",
    "status": "completed",
    "transcript": [
        {"role": "agent", "message": "..."},
        {"role": "user", "message": "Sí, voy a pagar <MONTO> el <FECHA>"},
    ],
    "analysis": {
        "evaluation_criteria_results": [{"name": "payment_promise", "result": True}],
    },
}

def send(api, conversation_id):
    ev = dict(EVENT); ev["conversation_id"] = conversation_id
    r = urllib.request.Request(f"{api}/webhooks/<proveedor>",
        data=json.dumps(ev).encode(),
        headers={"Content-Type": "application/json"})  # sin firma — ese es el punto
    with urllib.request.urlopen(r, timeout=15) as resp:
        return resp.status, resp.read().decode()

Disciplina del PoC real: el conversation_id usado en el PoC fue un identificador CLARAMENTE marcado como sintético ("PENTEST-FAKE-CALL-2026"), NUNCA el ID de una llamada real de un cliente/deudor real — para demostrar el impacto (el evento se acepta y se "bufferea") sin contaminar datos de negocio reales durante la prueba. Solo se corrió contra STAGING por defecto; PROD requería flag explícito.

Cómo probarlo en cualquier engagement con webhooks entrantes

  1. Identificar todos los endpoints que reciben webhooks de proveedores externos (Stripe, Twilio, ElevenLabs, GitHub, etc. — cada uno documenta su propio mecanismo de firma).
  2. Enviar el mismo payload que un evento legítimo, PERO sin el header de firma o con un valor inválido, usando un identificador de prueba claramente sintético.
  3. Si el endpoint procesa el evento igual (HTTP 200 y efecto observable en el sistema), es un hallazgo de "falta de verificación de autenticidad de webhook" — impacto según qué datos de negocio dependen de ese evento (no todos los webhooks tienen el mismo impacto; el de cobranza es alto porque toca dinero).

Mitigación real a recomendar: usar el SDK/librería oficial del proveedor para verificar firma + timestamp (ventana de replay, ej. 30 min) ANTES de cualquier procesamiento del payload — nunca implementar la verificación HMAC a mano si el proveedor ya ofrece un método probado.

Ver también: [[cheatsheet-de-dos-por-abuso-de-limites-ausentes-creacion-masiva-e-importacion-sin-limite|DoS por límites ausentes]] (mismo cliente/engagement), [[cheatsheet-de-ataques-a-jwt-algoritmos-y-forjado-de-tokens|cheatsheet de forjado de tokens]], [[indice-de-casos-reales-de-brechas-publicas-por-vector|índice de casos reales de brechas públicas]].