Ir al contenido principal
Laravel, shipping fast.

send() es corto, y la mayoría de sus líneas están ahí a causa de un ataque.

// app/Listeners/DeliverLicenseWebhook.php
private function send(
    Consumer $consumer,
    array $payload,
): ClientResponse {
    $body = json_encode($payload, JSON_THROW_ON_ERROR);
    $timestamp = now()->timestamp;

    $signature = hash_hmac(
        'sha256',
        "{$timestamp}.{$body}",
        $consumer->webhook_secret,
    );

    $header = "t={$timestamp},v1={$signature}";

    return Http::timeout(15)
        ->withoutRedirecting()
        ->withHeader('X-Signature', $header)
        ->withBody($body, 'application/json')
        ->post(PublicUrl::resolve($consumer->webhook_url));
}

La firma

Una vez que existe un webhook, cualquiera que averigüe la URL de un suscriptor puede enviarle un cuerpo que parezca tuyo. El suscriptor necesita una forma de comprobarlo. La respuesta estándar es un secreto que conocen los dos lados: firmas el cuerpo con él y envías la firma en una cabecera.

La marca de tiempo está dentro de la cadena firmada para que una petición capturada no pueda repetirse la semana que viene: el suscriptor rechaza cualquier cosa de más de unos pocos minutos. El secreto lo generas tú, se le muestra al consumidor una vez, y se guarda cifrado y oculto, como mostró el modelo de arriba.

El lado del suscriptor es una comparación, y es la línea que la gente hace mal:

// Mal: filtra información de tiempos
if ($received === $expected) {
    // ...
}

// Bien: tiempo constante
if (hash_equals($expected, $received)) {
    // ...
}

=== se detiene en el primer byte que difiere, así que un atacante que mida los tiempos de respuesta con suficiente precisión puede adivinar una firma carácter a carácter. hash_equals() tarda lo mismo difieran donde difieran las cadenas. Pon esto en la documentación de tus webhooks. Es el código del suscriptor el que tiene que hacerlo bien.

La dirección

withoutRedirecting() y PublicUrl::resolve() te protegen a ti. Una URL de webhook es una dirección que eligió un consumidor, y tu servidor está a punto de enviarle una petición desde dentro de tu red. Apúntala a http://169.254.169.254/ y, en la mayoría de las plataformas en la nube, tu servidor obtiene sus propias credenciales. Apúntala a http://localhost:6379 y tu servidor habla con tu Redis. Esto es falsificación de peticiones del lado del servidor (SSRF), y un webhook es la forma más común de construirla por accidente.

Valida la URL cuando se guarda. La regla del Capítulo 2 ganó max:255 y new PublicUrl en el listado de arriba, así que acepta solo HTTPS y solo hosts que resuelven a direcciones públicas.

Comprueba otra vez cuando se usa. El DNS puede cambiar después de guardada la URL, así que PublicUrl::resolve() vuelve a buscar el host en el momento de la entrega y lanza una excepción si alguna de las direcciones a las que resuelve es privada o reservada.

No sigas redirecciones. Sin withoutRedirecting(), una URL perfectamente pública puede responder con una redirección a una interna, y las dos comprobaciones no habrían servido de nada.

El Capítulo 19 muestra PublicUrl y es honesto sobre lo que no puede hacer. Si los webhooks son una parte grande de tu producto, envíalos a través de un proxy cuyo único trabajo sea rechazar destinos internos, y trata las comprobaciones de aquí como la segunda línea.

No se pudo cargar el audio. Inténtalo de nuevo en un momento.