Skip to main content

La importancia de verificar las firmas de los webhooks

Escrito por

Marcelo Oliveira

blog feature playwright tests

29 de junio de 2023

0 minutos de lectura

Los webhooks son una técnica de integración mediante callbacks para enviar y recibir información, como notificaciones de eventos, casi en tiempo real. Pueden activarse por eventos de una aplicación y transmitir datos por HTTP a otra aplicación o API de terceros. Puedes configurar una URL de webhook y conectar participantes externos para personalizar, ampliar o modificar flujos de trabajo.

Los webhooks pueden estar firmados o no. Sin embargo, por motivos de seguridad, se recomienda incluir una firma verificable para que el receptor pueda confirmar que la solicitud proviene de la fuente de webhook esperada.

Estos son algunos ejemplos de cómo se usan los webhooks actualmente:

  • Notificaciones en una plataforma de control de versiones basada en Git cuando un desarrollador envía código al repositorio.

  • Notificaciones cuando un usuario responde a un hilo de mensajes en una plataforma de mensajería.

  • Notificaciones de un servicio de pagos que confirman que se autorizó un método de pago para una compra minorista.

Según un informe de Verizon, los ataques a la cadena de suministro representaron el 62 % de todos los incidentes de intrusión en sistemas en 2022. A lo largo del ciclo de vida del desarrollo de software, es fundamental mantener la seguridad de todos los componentes y prácticas que intervienen en el desarrollo y la implementación. Si se explotan las vulnerabilidades, pueden poner en riesgo a los socios involucrados y afectar toda la cadena de suministro de software.

Para hacer frente a estas amenazas y mantener la seguridad de la cadena de suministro, los proveedores de la cadena de suministro aplican firmas de webhook. Por ejemplo, con CircleCI puedes usar firmas de webhook para validar un secreto y asegurarte de que solo CircleCI, y no un actor malicioso, invoque tu webhook. Travis CI también proporciona un encabezado HTTP de firma en su proceso de verificación de webhooks.

En este recorrido, implementaremos un webhook de GitHub en Node.js que detecta cuándo los usuarios envían código a un repositorio.

Requisitos previos

Para seguir los pasos, necesitarás hacer lo siguiente:

  • Descargar e instalar Node.js y npm. 

  • Instalar ngrok, una herramienta de proxy inverso que abre un túnel seguro desde una URL pública hasta tu aplicación Node.js local.

  • Instalar Postman, una plataforma que te permite crear solicitudes HTTP e inspeccionar fácilmente los resultados.

Verificar firmas de webhook con Node.js

Crea una carpeta nueva en tu equipo y un archivo nuevo llamado webhook.js con el siguiente código. Recuerda reemplazar el valor de la constante repo por la ruta de tu repositorio local:

const http = require('http');
const exec = require('child_process').exec;

const repo = "C:\\Users\\your-user-name\\Documents\\GitHub\\webhook-test";

http
  .createServer((req, res) => {
    req.on('data', chunk => {

      const body = JSON.parse(chunk);

      console.log(body);

      const isMain = body?.ref === 'refs/heads/main';

      if (isMain) {
        try {
            console.log('Push event detected. Pulling repository updates...');
            exec(`cd ${repo} && git fetch && git pull`);
            console.log('Git repository pulled successfully.');
        } catch (error) {
          console.log(error);
        }
      }
      res.end();
    });
  })
  .listen(8080);

Ahora, ejecuta la siguiente línea en tu terminal:

node webhook.js

Esto es lo que buscamos lograr con el archivo webhook.js:

  1. La constante repo almacena la ruta a la carpeta que contiene el repositorio que queremos monitorear.

  2. La función createServer inicia un servidor HTTP en nuestro equipo, en el puerto 8080. La aplicación del webhook ya está en ejecución y lista para recibir solicitudes en el endpoint http://localhost:8080.

  3. Una vez que se procesa la solicitud entrante, la constante body almacena la mayor parte de su contenido. Una solicitud a nuestro endpoint significa que recibimos una notificación del webhook de GitHub en nuestra URL local de carga útil, http://localhost:8080, que nos informa que alguien envió código al repositorio que estamos monitoreando.

  4. Queremos asegurarnos de que el envío se haya realizado específicamente a la rama main. Para verificarlo, analizamos los datos JSON de la solicitud en un objeto body y usamos la constante isMain para indicar si la referencia coincide con la rama main.

  5. Si se acepta la solicitud, ejecutamos un comando para cambiar al repositorio local. Por último, ejecutamos los comandos git fetch y git pull para actualizarlo.

Simular una solicitud maliciosa con Postman

Estamos diseñando la aplicación Node.js para que funcione con GitHub. Sin embargo, antes de usar GitHub, simulemos una solicitud maliciosa con Postman para mostrar lo vulnerable que es nuestra aplicación. Esto te ayudará a comprender la importancia de las firmas.

Abre la aplicación Postman y configura una nueva solicitud HTTP POST a nuestra URL local de carga útil: http://localhost:8080.

En la pestaña Body, agrega el siguiente código:

{
    "ref": "refs/heads/main",
    "comment": "hi, I'm a malicious request..."
}
Solicitud de Postman que muestra una carga útil de webhook y una respuesta 401 Unauthorized que indica que la firma del webhook no coincide con el hash de la carga útil.

Haz clic en Send. Abre la terminal donde se está ejecutando Node.js y observa qué ocurrió:

Salida de terminal que muestra la detección de un evento de Git push, la descarga de actualizaciones del repositorio y la descarga exitosa del repositorio.

Como puedes ver, la aplicación Node.js acepta una solicitud de una fuente que no es GitHub, lo cual es peligroso. Necesitamos implementar firmas de webhook en nuestro código para verificar si quien realiza la solicitud es legítimo.

Configurar ngrok

En este escenario, ejecutamos la aplicación Node.js de forma local. Sin embargo, GitHub no puede acceder al endpoint http://localhost:8080 de tu equipo para enviar una solicitud de webhook. Podemos resolverlo publicando la aplicación Node.js en un servidor público. No obstante, es más fácil ejecutar y depurar Node.js en el equipo de desarrollo que implementarlo en un servidor web o en la nube, así que usaremos ngrok. ngrok es una herramienta de proxy inverso que abre un túnel seguro desde una URL pública aleatoria hasta el endpoint local del webhook.

Abre una ventana de terminal en tu equipo y ejecuta el siguiente comando de ngrok para exponer el puerto local 8080:

ngrok http 8080

ngrok confirmará que expuso el puerto local 8080 mediante un túnel seguro con una URL pública aleatoria:

Forwarding https://<<YOUR-RANDOM-SUBDOMAIN>>.sa.ngrok.io -> http://localhost:8080

A continuación, usaremos el subdominio público de ngrok que aparece arriba para configurar un webhook de GitHub.

Configurar un webhook de GitHub

Visita la página de GitHub para crear un repositorio nuevo y agrega un repositorio nuevo para configurar el webhook:

Formulario de creación de repositorio con el título “Create a new repository”, un propietario oculto y el nombre del repositorio “webhook-test”.

Haz clic en Settings en la barra superior. Luego, selecciona la opción Webhooks en la lista de la izquierda y haz clic en Add webhook:

Página de configuración del repositorio de GitHub con Webhooks seleccionado en la barra lateral y flechas que apuntan al encabezado Webhooks y al botón Add webhook.

Se abrirá el formulario Add webhook, donde configuraremos un webhook para un evento en el que un desarrollador envía código al repositorio.

Formulario de configuración de Agregar webhook de GitHub que muestra una URL de carga útil, el tipo de contenido JSON, el campo de secreto, las opciones de activación de eventos y la casilla Activo.

Ahora, completa el formulario de la siguiente manera:

  • En el campo Payload URL, ingresa la dirección de ngrok correspondiente al endpoint de tu aplicación local de Node.js.

  • En el campo Content type, selecciona «application/json» para indicar el tipo de contenido que GitHub enviará al endpoint.

  • En el campo Secret, escribe «some-webhook-secret». Recuerda que, en producción, necesitarás contraseñas o frases más seguras.

  • En Which events would you like to trigger this webhook, selecciona Just the push event. Así configurarás el webhook de GitHub para una acción de envío al repositorio que podrás activar para probar la verificación.

Nota: Siempre se recomienda habilitar la verificación de Secure Socket Layer (SSL) en la configuración del webhook. Al habilitarla, las organizaciones se aseguran de que los datos transmitidos entre sistemas estén protegidos y no puedan ser interceptados por partes no autorizadas.

Cuando hayas completado el formulario, haz clic en Add webhook.

Formulario de GitHub para agregar un webhook, con flechas que destacan la URL de carga útil, el tipo de contenido JSON, el secreto y la selección del evento push.

Proteger el webhook

Necesitamos agregar el módulo crypto de Node.js para manejar datos cifrados. Agrega esta línea al primer bloque del archivo webhook.js:

const crypto = require('crypto');

A continuación, declara una constante para almacenar la misma cadena secreta que configuraste en tu webhook de GitHub:

const secret = 'some-webhook-secret';

Nota: Nunca se recomienda codificar los secretos directamente. Este método es solo para fines demostrativos.

Al comienzo de la función req.on, declaramos una constante para almacenar el valor de la firma SHA256 calculada con HMAC. Obtén la firma HMAC-SHA256 ejecutando una función hash criptográfica SHA256 sobre la clave secreta compartida y el valor enviado en la solicitud de GitHub (representado aquí por el parámetro chunk):

    req.on('data', chunk => {

      const hashAlgorithm = 'sha256'
      const signature = Buffer.from(req.headers['x-hub-signature-256'] || '', 'utf8')
      const hmac = crypto.createHmac(hashAlgorithm, secret)
      const digest = Buffer.from(hashAlgorithm + '=' + hmac.update(chunk).digest('hex'), 'utf8')

A continuación, declaramos la constante isAllowed dentro del evento req.on para indicar si el encabezado x-hub-signature-256 coincide con la firma que calculamos. Usa la función crypto.timingSafeEqual, que compara dos variables sin revelar información de tiempo que pudiera ayudar a un atacante a adivinar alguno de los valores:

      const isAllowed = (signature.length === digest.length && crypto.timingSafeEqual(digest, signature));
      if (!isAllowed) {
        const msg = 'Webhook signature does not match the hash of the payload';
        console.log(msg);
        res.writeHead(401)
        res.end(msg)
        return;
      }

En el código anterior, si hay una coincidencia, podemos tener la certeza de que la solicitud proviene de GitHub y no de un actor malicioso. Si la solicitud no proviene de GitHub, se devuelve una respuesta HTTP 401 Unauthorized.

Estas son algunas prácticas recomendadas que se aplican en este código y que deberías implementar al desarrollar tus webhooks:

  • Al realizar operaciones con cadenas para calcular las firmas del webhook, siempre procesa la carga útil como UTF-8, ya que puede contener caracteres Unicode. Además, si tienes expectativas sobre cómo deberían verse tus datos, puedes implementar un método de validación para asegurarte de que los datos recibidos tengan el formato esperado.

  • Algunos proveedores de webhooks (como GitHub) pueden incluir un encabezado x-hub-signature en las solicitudes. El encabezado x-hub-signature se genera con SHA-1, que se considera un algoritmo hash débil, pero solo se incluye por compatibilidad con versiones anteriores. Siempre que sea posible, usa el encabezado x-hub-signature-256, que se genera con el algoritmo SHA-256, más seguro.

  • Inicia la firma hash con "sha256=" usando el secreto del cuerpo de la carga útil de la solicitud.

  • En lugar de usar un operador de igualdad simple ("==" o "==="), utiliza un método de comparación seguro que reduzca la probabilidad de sufrir ataques de temporización. Todos los lenguajes y plataformas modernos cuentan con al menos uno de estos métodos seguros. En este proyecto de Node.js, usamos la función crypto.timingSafeEqual.

Puedes obtener el código completo de webhook.js aquí.

Para obtener información más detallada, consulta la documentación de GitHub sobre cómo proteger los webhooks.

Probar el endpoint del webhook protegido

Como modificaste el código, debes reiniciar tu servidor local de Node. En la consola, detén el servidor de Node con Ctrl+C (o Command+C en Mac) y vuelve a ejecutar el comando node webhook.js.

Luego, vuelve a la aplicación Postman y reenvía la solicitud. Ahora, la aplicación Node.js responde con el código HTTP 401 Unauthorized:

Solicitud de Postman que muestra una carga útil JSON maliciosa y una respuesta 401 No autorizado que indica que la firma del webhook no coincide con el hash de la carga útil

Abre GitHub y agrega un archivo README.md si aún no lo tienes, o ábrelo si ya existe. Luego, agrega o modifica el texto como se muestra a continuación:

Editor de README.md de GitHub que muestra un repositorio llamado webhook-test con el texto «Mi prueba de webhook» y «Esta es una prueba de webhook».

Por último, haz clic en Commit changes para enviar los cambios a la rama main del repositorio:

Cuadro de diálogo de GitHub «Confirmar cambios» que muestra el mensaje «Actualizar README.md» y la opción para confirmar directamente en la rama principal, señalada con una flecha morada.

Ahora, vuelve a la terminal de Node.js para confirmar que el repositorio se haya obtenido y actualizado correctamente:

Salida de terminal que muestra un evento push de un webhook de GitHub, una actualización del repositorio y una extracción (pull) exitosa con Git.

Conclusión

Los webhooks permiten la comunicación entre herramientas y proporcionan datos en tiempo real cuando ocurren eventos. Puedes integrarlos en sistemas para crear flujos de trabajo colaborativos flexibles y ampliables, algo que no sería posible con API que tienen parámetros predefinidos.

Como están abiertos por diseño, los webhooks son vulnerables a amenazas de seguridad, por lo que es esencial tomar las medidas adecuadas para prevenir ataques a la cadena de suministro.

Las firmas de webhook son la solución recomendada, ya que los proveedores de webhooks pueden configurar secretos y generar firmas con algoritmos hash seguros. Las firmas son una forma sencilla y segura de garantizar que la comunicación de datos entre webhooks ocurra únicamente entre socios verificados y de confianza.

Comienza con Capture the Flag

Aprende a resolver desafíos de Capture the Flag con nuestro taller virtual 101 a pedido.

Leer más

blog feature snyk container custom base recommendations
Blog

Presentamos Evo Agentic AppSec: seguridad que sigue el ritmo del desarrollo con IA

El desarrollo impulsado por IA avanza más rápido que los flujos de trabajo de seguridad tradicionales. Agentic AppSec de Snyk usa agentes de seguridad para encontrar, corregir y verificar vulnerabilidades de forma continua, mientras las personas establecen el nivel.

blog feature ai violet
Blog

VulnBump: ¿Pueden los agentes de IA corregir dependencias vulnerables sin romper la compilación?

VulnBump prueba agentes de IA con 307 actualizaciones reales de dependencias. Una corrección cuenta solo si elimina la vulnerabilidad y la compilación y las pruebas siguen pasando.

Blog

Los modelos de frontera encontraron las vulnerabilidades. Solo el atacante encontró las cadenas.

El análisis estático encontró las fallas, pero solo las pruebas de ataque en vivo demostraron cómo podían encadenarse para provocar brechas. Una comparación de Evo COS, Claude Security y Claude Code Security.