Saltar al contenido principal

ArtinTech Solution

Stripe webhooks failing with 403 behind Cloudflare and the fix

El correo llegó un domingo por la mañana, reenviado por la directora de operaciones de una fundación sin ánimo de lucro europea con una sola línea: por favor, revisa lo de abajo y actúa. Debajo había un mensaje automático de Stripe. Llevaba varios días sin poder entregar eventos webhook al sitio de donaciones, diecinueve intentos habían fallado y, si nada cambiaba, Stripe dejaría de enviar notificaciones a ese endpoint en menos de una semana.

Para una organización que recibe donaciones únicas y mensuales con WordPress, no es un aviso menor. Los webhooks son la forma en que la web se entera de que un pago se completó. Sin ellos, las donaciones se quedan pendientes, los donantes no reciben su recibo y las renovaciones mensuales no se registran, aunque el dinero siga llegando a Stripe.

Esta es la historia de cómo rastreamos el fallo, por qué el primer problema que encontramos era real pero no era la causa, y el pequeño cambio que lo resolvió sin desactivar ninguna medida de seguridad.

Qué hace un webhook de Stripe y por qué importa que falle

Cuando un donante paga, el navegador va a Stripe y vuelve. La web no puede fiarse de lo que el navegador dice sobre el pago, así que Stripe también llama directamente al sitio, de servidor a servidor, con un evento como payment_intent.succeeded. El sitio debe responder con un código HTTP entre 200 y 299. Cualquier otro cuenta como fallo, y Stripe reintenta con intervalos crecientes durante hasta tres días antes de abandonar ese evento.

En este caso el plugin de donaciones era GiveWP, que escucha en una dirección terminada en ?give-listener=stripe. El sitio es multilingüe, así que había dos endpoints activos, uno bajo /de/ y otro bajo /fr/. Los dos fallaban, aunque el correo de Stripe solo mencionaba uno.

Registro de entregas de webhooks con errores 403 repetidos
Todos los intentos de entrega desde principios de mes habían devuelto 403. (Ilustración, datos anonimizados.)

Primer hallazgo: un problema real, pero no la causa

El escritorio de WordPress mostraba un error de activación. El complemento Recurring Donations se había desactivado porque necesitaba una versión más reciente de GiveWP que la instalada. El complemento se había actualizado y el plugin principal no, y WordPress lo había desactivado sin avisar. Además, su licencia no se iba a renovar, así que ya no recibía actualizaciones.

Parecía la explicación perfecta, y había que arreglarlo de todos modos. Hicimos una copia de seguridad completa, actualizamos GiveWP y reactivamos el complemento. La parte pública del sitio se cayó de inmediato con un error fatal. El registro apuntaba a una ventana emergente que incluía el shortcode de un formulario antiguo que ya no existía. Desactivamos el complemento para recuperar el sitio, corregimos la ventana emergente, lo reactivamos y comprobamos la portada, la página de donaciones y todas las versiones de idioma.

El sitio volvía a estar sano y la opción de donación mensual había vuelto. Los webhooks seguían fallando.

Leer el error real en lugar de adivinar

La forma más rápida de dejar de adivinar es abrir una entrega fallida en el Dashboard de Stripe, en Desarrolladores, Webhooks, y mirar dos cosas: el código de estado y el cuerpo de la respuesta. Aquí el estado era 403 Forbidden y el cuerpo no era un error de WordPress. Era una página HTML titulada Just a moment… que cargaba scripts de challenges.cloudflare.com.

Cuerpo de respuesta de un webhook fallido con la página de desafío de Cloudflare
El cuerpo de la respuesta lo delata: una página de verificación humana servida por la CDN, no por WordPress. (Ilustración.)

Esa página es el desafío de navegador de Cloudflare. Pide al navegador que demuestre que es humano antes de dejar pasar la petición. Una persona con Chrome lo supera en un segundo. Los servidores de Stripe no son un navegador, no pueden resolverlo, y por eso cada webhook se detenía en el borde. La petición nunca llegaba a WordPress, y por eso no aparecía nada en los registros del sitio.

Encontrar qué función de Cloudflare bloqueaba a Stripe

Cloudflare puede desafiar tráfico desde varias funciones, y la solución depende de cuál sea la responsable. En Seguridad, Analíticas, Eventos filtramos por la cadena de consulta give-listener. Todas las peticiones mostraban la acción Managed Challenge y el servicio Security level.

Eventos de seguridad de Cloudflare con peticiones de webhook desafiadas por Security Level
Las llamadas del proveedor de pagos se desafiaban igual que a cualquier visitante. (Ilustración.)

La misma lista mostraba otra cosa: visitantes normales de Suiza, Francia y Alemania también recibían el desafío en páginas corrientes. El nivel de seguridad de la zona estaba muy alto, probablemente por un ataque anterior, y nadie se había dado cuenta de que también frenaba al proveedor de pagos.

La solución: una regla Skip acotada, no un sitio más débil

Bajar el nivel de seguridad habría funcionado, pero habría cambiado la protección de todo el sitio para resolver un problema de una sola URL. La respuesta limpia es una regla personalizada que deja pasar solo la dirección del webhook y mantiene todo lo demás igual.

  1. Ve a Seguridad, Reglas de seguridad y crea una nueva regla personalizada.
  2. Ponle un nombre claro, por ejemplo Allow Stripe webhooks.
  3. Configura la condición: URI Query String, contains, give-listener=stripe.
  4. Elige la acción Skip y marca All remaining custom rules.
  5. Abre More components to skip y marca Security Level y Browser Integrity Check.
  6. Coloca la regla la primera de la lista y despliégala.
(http.request.uri.query contains "give-listener=stripe")
Regla Skip de Cloudflare para give-listener=stripe con Security Level marcado
La regla solo afecta al listener del webhook. Security Level debe marcarse expresamente, porque es la función que bloquea. (Ilustración.)

El detalle que se escapa con facilidad es el paso cinco. La primera versión de la regla solo omitía Browser Integrity Check, la vista previa de la API mostraba "products": ["bic"] y los webhooks habrían seguido fallando. El registro de eventos decía que el bloqueo venía de Security level, así que ese es el componente que hay que omitir. Una nota para el plan gratuito: si el registro indica Bot Fight Mode, una regla Skip no puede saltarlo y hay que desactivar ese ajuste.

¿Es seguro? La regla no confía ciegamente en la petición. GiveWP sigue verificando cada webhook con la clave de firma de Stripe, así que una petición falsificada que llegue al listener la rechaza WordPress. Autenticar a Stripe nunca fue tarea de Cloudflare; es tarea de la firma.

Confirmar la solución y recuperar las donaciones perdidas

Reenviamos un evento fallido desde el Dashboard de Stripe y volvió con 200 OK. En menos de una hora los reintentos automáticos empezaron a funcionar y el panel marcó los fallos anteriores como Delivery recovered. Al día siguiente Stripe envió un correo confirmando que el endpoint se había recuperado.

Entregas de webhooks devolviendo 200 OK tras la corrección
Con la regla activa, las entregas devolvían 200 OK y los reintentos de Stripe vaciaron la cola. (Ilustración.)

Dos apuntes prácticos de la limpieza. Primero, el botón de reenvío solo funciona en el intento más reciente de cada evento, así que para los eventos que Stripe ya dio por perdidos hay que abrir el propio evento y reenviarlo desde ahí. Segundo, cuadra el dinero, no solo los webhooks: compara cada pago de Stripe desde que empezó el problema con las donaciones registradas en WordPress. Aquí todos los pagos correctos ya estaban registrados, uno había fallado de verdad por fondos insuficientes y se marcó como fallido, y se identificaron dos pequeños pagos de prueba hechos durante la investigación para que nadie los contara como donaciones.

Lecciones para cualquier WordPress que acepta pagos

  • Lee primero el cuerpo de la respuesta. Un 403 que contiene HTML de tu CDN no es un fallo de la aplicación y ninguna depuración de plugins lo arreglará.
  • La seguridad y los webhooks de pago tienen que ponerse de acuerdo. Subir el nivel de seguridad tras un ataque es razonable; olvidar que también afecta a las llamadas entre servidores es como se pierden donaciones.
  • Permite la ruta, no el mundo. Una regla Skip acotada al listener mantiene protegido el resto del sitio.
  • Las licencias caducadas son un riesgo lento. Cortan las actualizaciones y la siguiente actualización del núcleo puede desactivar el complemento sin aviso.
  • Vigila las alertas. Stripe avisa durante una semana aproximadamente antes de desactivar un endpoint. Asegúrate de que esos correos llegan a alguien que pueda actuar.

Escribimos sobre un caso parecido, en el que la caché y WordPress no coincidían en el tiempo, en un error 403 intermitente causado por el TTL de la caché, y sobre las revisiones que detectan estos problemas a tiempo en qué incluye realmente el mantenimiento web.

Preguntas frecuentes

¿Por qué mis webhooks de Stripe devuelven 403 si la web funciona en mi navegador?

Porque tu navegador supera el desafío de Cloudflare y los servidores de Stripe no. Si el cuerpo de la respuesta fallida menciona Just a moment o challenges.cloudflare.com, la petición se detiene en Cloudflare antes de llegar a WordPress.

¿Debería añadir las IP de Stripe a una lista blanca?

Se puede, con la lista de IP que publica Stripe, pero cambia y hay que mantenerla. Una regla Skip en la ruta del webhook es más sencilla y, como el plugin verifica la firma, igual de segura en la práctica.

¿Pierdo donaciones mientras fallan los webhooks?

Stripe normalmente cobra el dinero, pero la web no lo registra. Las donaciones quedan pendientes, no se envían recibos y faltan las renovaciones recurrentes hasta que se entregan los eventos o se corrigen los registros a mano.

¿Cuánto tiempo sigue reintentando Stripe?

En modo producción, hasta tres días con intervalos crecientes. Si un endpoint sigue fallando, Stripe envía un aviso por correo y acaba desactivándolo.

¿Gestionas donaciones, una tienda o membresías en WordPress y recibes alertas de pago que no terminas de entender? Hacemos exactamente este tipo de investigación dentro de nuestros servicios de desarrollo WordPress y mantenimiento. Hablemos y te diremos con claridad qué falla y qué hace falta para arreglarlo.

Share this article

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *