En resumen
El fraude en pagos cripto vive en el hueco entre que alguien dice que ha pagado y el pedido sale. Ahí caben siete intentos y ninguno necesita romper una cadena: un hash que no paga ninguna factura, una transferencia que llega a la dirección sin cerrarla, una devolución apuntada a otra dirección, un navegador al que se le cree el resultado o el precio, un aviso falsificado, uno reenviado, y la propia cuenta del comercio. Cada uno tiene un control que lo para, y cada control tiene un sitio donde se acaba: la firma que no mira la marca temporal, la verificación reforzada que un usuario sin segundo factor no llega a ver nunca.
Contra un pago en cripto el fraude no puede ser una reversión. Cuando una transferencia alcanza finalidad no hay esquema al que recurrir ni emisor que le cargue el importe al comercio. Lo que queda es la ventana anterior al envío: los minutos entre que alguien dice que ha pagado y un almacén, un servidor de licencias o un agente de soporte se lo cree.
Ahí viven siete intentos, ordenados abajo por lo que le cuestan a quien los hace: los tres primeros no necesitan más que un mensaje bien escrito. Cada uno lleva pegado el control que lo detiene y el punto donde ese control se acaba, porque un control descrito sin su borde es justo el que alguien acaba usando más allá de él.
Casi nada de esto es trabajo de blockchain. Un mensaje falsificado, un cliente en el que se confía, un pago desviado: técnicas viejas apuntadas a un cobro en vez de a un formulario de acceso.
El hash que no prueba nada
Llegan una captura y un hash de transacción, y el hash existe de verdad.
Ahí está la trampa. Lo pegas en un explorador de bloques y sale una transferencia real, con su importe y su símbolo de token al lado. Ninguna de las tres cosas la ata a esta factura, y el símbolo es la más floja: en EIP-20, name y symbol son opcionales, y el estándar le dice a las interfaces que «MUST NOT expect these values to be present».
Ese símbolo es una cadena de texto que eligió quien escribió el contrato.
El control es el estado de la propia factura. Un pago se abona cuando una transferencia llega a la dirección de esa factura y confirma a la profundidad fijada para su red y su importe: dos bloques en Ethereum hasta 100 $, treinta y dos por encima de 10.000 $, mientras que seis redes liquidan por finalidad de red en lugar de por recuento de bloques. Nadie tiene que traer la captura: mientras espera, quien paga ve en la página de pago cada transferencia detectada, incluidas las que aún no han confirmado.
La profundidad es un canje contra la finalidad absoluta y no una prueba de ella: si una reorganización deja huérfana una transferencia ya abonada, el abono no se te quita y el descuadre se queda del lado de la plataforma. La factura contesta si llegó el dinero. Nunca si este pedido merece salir.
¿Por qué entra dinero sin que la factura se cierre?
Porque una transferencia puede llegar a la dirección y no pagar ninguna factura.
Cuatro de las formas que toma las produce cualquiera a mano: una transferencia enviada con la ventana de pago ya cerrada, otra apuntada a una factura pagada, vencida o cancelada, otra en otro activo —USDC contra una factura en USDT— y otra que aterriza en una dirección sin ninguna factura detrás. Cada una se abona al saldo del comercio menos la comisión de procesamiento de siempre.
Lo que nada de eso toca es la factura. Se queda sin pagar, y no sale ningún webhook: un abono de esta clase no tiene evento ninguno, así que un sistema que solo escucha el flujo no se entera nunca. Una persona sí. El aviso llega al correo y a la campana del panel, y en la página de saldos queda el bloque con el motivo, el importe y la comisión.
El mensaje del comprador lleva entonces una frase verdadera dentro. Mira tu saldo, el dinero está ahí. Está. Un agente que comprueba la llegada y suelta el pedido está cerrando algo que la factura sigue marcando como no pagado.
Un matiz juega a favor del comercio: el mismo activo desde otra red aceptada paga la factura uno a uno donde la dirección de esa factura lo admite. No todas lo admiten, y el dinero enviado por una cadena que esa dirección no cubre se queda donde cayó.
El límite es romo: el dinero ya llegó al comercio, y sacarlo de ahí es una transferencia de salida que alguien tiene que autorizar. No hay devolución autogestionada ni promesa de retorno en la capa de pago.
La devolución que aterriza en otra dirección
Este intento no le cuesta nada a quien lo hace y no toca ninguno de tus sistemas.
Llega una petición de devolución con el destino ya escrito dentro: desde el buzón de un cliente que lee otra persona, o de parte de un compañero al que han convencido por teléfono.
El control es la lista blanca de retiros. Un retiro solo puede nombrar una dirección que ya esté en ella, y una lista vacía desactiva los retiros por completo. La lista guarda cada entrada por dirección y familia de redes, no por red suelta: una dirección EVM aprobada una vez vale para todas las redes EVM de salida. Quitar una entrada la revoca y no la borra, con lo que el rastro queda entero. Añadir una dirección es además una acción privilegiada y cae en ese mismo rastro.
Se queda corto en dos sitios. La verificación reforzada al cambiar la lista no es incondicional: se salta para un usuario del comercio que no tiene segundo factor, porque refuerza un factor que ya existe. No la apuntes como defensa que ya tienes. Empieza a existir el día en que alguien activa TOTP o una passkey.
El otro sitio es el tamaño de una sola aprobación. Hasta mil direcciones entran en una petición bajo una única verificación, y una importación por CSV lo aprovecha con un aviso por archivo en lugar de uno por dirección. La comodidad y el radio de daño son el mismo número, así que lo que hay que leer de vez en cuando es la lista. Tampoco dice de quién es la billetera del otro extremo: una dirección de cliente puede estar ahí porque el comercio la puso adrede. Quién aprueba, con qué plazos y bajo qué criterio es materia de una política de devoluciones.
¿Quién le dijo a tu tienda que estaba pagado?
El navegador, casi siempre. Un testigo con interés en el resultado.
Son dos versiones del mismo defecto. En la primera, el pedido se marca como pagado en la URL de retorno o con un evento de JavaScript de la ventana de pago: el SDK de bajo código dispara cinco, todos CustomEvent sobre window, y cuentan lo que hizo la ventana. Sirven para mover un indicador de carga. No son el estado del pago, y cualquiera con la consola abierta los dispara.
En la segunda, el importe sale de la página. El widget lo resuelve de cinco fuentes por orden de prioridad, y no son igual de seguras: un price_id es un precio guardado en el servidor, así que la petición no lleva importe ninguno, mientras que amount_selector lee el .value de un elemento del DOM. En una página que el comprador puede editar, eso es el comprador poniendo el precio. MITRE archiva la clase como CWE-602, imposición del lado del cliente de seguridad del lado del servidor: un servidor que «relies on the client to implement a mechanism that is intended to protect the server».
Mover el precio al servidor no vuelve segura una página de pago; saca una decisión de su alcance. Un carrito cuyo total cambia con cada pedido lo sigue teniendo que valorar tu código, y el estado de pagado tiene que venir de un sitio donde el comprador no pueda escribir.
El aviso que no salió de aquí
Una URL de notificación es una dirección de internet pública, y acepta JSON.
Un invoice.paid falsificado no necesita acceso a nada: la forma correcta, el id de factura correcto y un handler que se crea el cuerpo. CWE-345 es la entrada de esa clase, un producto que «does not sufficiently verify the origin or authenticity of data, in a way that causes it to accept invalid data».
Cada entrega va firmada con HMAC-SHA256, y la cabecera X-Webhook-Signature es compuesta: una marca temporal y uno o más valores v1. Durante una rotación de secreto lleva dos durante 24 horas y vale cualquiera de los dos, y por eso un receptor lee v1 como una lista y no como un campo. La página de la firma trae qué se hashea y la comparación en tiempo constante; este artículo se queda con la decisión de confianza.
Queda el reenvío, que es donde esto sorprende. X-Webhook-Timestamp viaja en cada entrega y al salir no se impone ninguna ventana: la tolerancia es de quien verifica. La documentación pide rechazar por encima de 300 segundos y los SDK oficiales traen cinco minutos por defecto, así que quien construye sobre uno hereda la comprobación. Un verificador escrito a mano contra el HMAC a secas acepta una semana después la entrega que capturaron hoy, y la firma que lleva encima es auténtica.
El mismo evento, dos envíos
La mayoría de los duplicados no son un ataque, y eso es lo que hace tan fácil escribir mal el handler.
Un intento que agota sus diez segundos se reintenta. Un ciclo entero son once intentos repartidos por unas dieciséis horas, y la espera entre uno y otro crece de un minuto a ocho; después, un evento fallido lo reenvías tú a mano. Ni siquiera hace falta un fallo: dos endpoints suscritos a la misma categoría reciben dos entregas de un solo hecho de negocio. A quien ataca le basta con reenviar una entrega capturada.
En una petición viajan dos identificadores, y el control consiste en darle a cada uno su oficio. X-Webhook-Id —el evt_…— identifica la entrega, así que descarta por ahí. El inv_… de dentro de data identifica el negocio, idéntico entre endpoints y en todas las transiciones de estado, así que apunta el pedido contra ese. Cambiarlos de sitio falla por los dos lados: con el id de entrega como clave del pedido, dos endpoints apuntan un mismo pago dos veces; descartando por el id de factura, se pierde el paid que viene detrás de confirming. Por qué se repite un webhook de pago recorre el handler entero.
El descarte corta la segunda ejecución, no la segunda llegada: las entregas siguen viniendo mientras dure el ciclo. Cuando el ciclo se agota, lo que queda marcado como fallido es el evento; el endpoint sigue en pie. Y un confirming que se pasó horas en el escalón puede aterrizar detrás del paid que vino después, así que un handler que aplica lo último que llegó camina un pedido pagado hacia atrás.
Cuando ya está dentro de tu cuenta
Todos los controles de arriba responden ante la cuenta que los configuró.
Desde dentro del panel, la URL de notificación se edita, el secreto de webhook se rota y la lista blanca es un formulario. Ninguno de los seis intentos anteriores hace falta cuando la sesión es de otro.
No hay contraseña que pescar. Se entra con un enlace por correo, con Google, con Telegram OIDC donde esté activo o con una passkey, y TOTP queda disponible como segundo factor. Para este ataque la passkey es la más fuerte de las cuatro, porque está atada al sitio donde se creó: WebAuthn acota la credencial a la parte confiante con la que se registró, y el autenticador «ensures that all operations are scoped to a particular origin, and cannot be replayed against a different origin, by incorporating the origin in its responses».
Un dominio clonado, por convincente que sea, no recoge nada que pueda usar.
La verificación reforzada se planta delante de tres operaciones —desactivar el segundo factor, cambiar la lista blanca, editar la lista de IP de una clave de API— y cada ámbito es un ticket aparte, así que un código capturado para uno no autoriza ningún otro. Las credenciales de retiro llevan además una exigencia que las de cobro ni siquiera pueden pedir: sin lista de IP configurada, esa clave no autentica.
Que no haya contraseña no quiere decir que no haya sesión. El enlace por correo aterriza en un buzón, y el buzón es lo primero que se toma; el segundo factor cierra ese camino y no viene activado de serie. Lo que el comercio puede leer después son sus propias acciones registradas y su historial de accesos, porque el registro completo de la plataforma es de operadores.
¿Qué mitad te toca?
Los siete controles no están todos del mismo lado de la API.
Tres aguantan haga lo que haga cualquiera: el estado de la factura, la lista blanca por delante de un retiro, y la negativa a cerrar una factura con una transferencia que no la pagó. Los otros cuatro existen si los escribe tu código —verificar la firma y su marca temporal, descartar por el id de entrega, poner el precio en el servidor, activar el segundo factor que enciende la verificación reforzada— y cada uno es una tarde.
Queda un caso fuera de la lista. Una firma dice que una clave autorizó la transferencia, nunca que su dueño quisiera hacerlo, y nada del lado del comercio distingue una billetera robada de un buen cliente. Eso es un proceso de soporte y una política de envíos, no un control, y ninguna página de seguridad lo va a cerrar por ti.
| El intento | Qué lo para | Qué sigue abierto | |
|---|---|---|---|
| Quien paga aporta su propia prueba de pago | El estado de la factura, abonada a la profundidad configurada | Si el pedido merece salir sigue siendo una decisión comercial | |
| Una transferencia llega a la dirección y no paga nada | La factura no se mueve y el abono entra con su motivo | No sale ningún webhook, así que una integración de eventos no se entera | |
| La devolución apunta a una dirección aportada | Retiros solo a la lista blanca; lista vacía, retiros desactivados | Una sola verificación puede aprobar mil direcciones de golpe | |
| El envío o el precio se deciden en el navegador | Estado de pagado leído del servidor y precio guardado en el servidor | Un carrito variable lo sigue teniendo que valorar tu código | |
| Un aviso falsificado | Firma HMAC-SHA256, comparada en tiempo constante, v1 leído como lista | El reenvío: la ventana temporal la elige quien recibe | |
| Un evento entregado más de una vez | Descarte por X-Webhook-Id y atribución por el id de dentro de data | Las llegadas siguen; lo que se corta es la segunda ejecución | |
| Alguien que ya entró en tu cuenta | Sin contraseña que pescar: passkeys, TOTP, verificación por ámbito | El segundo factor lo activas tú, y sin él no hay verificación |
Preguntas frecuentes
¿Qué es el fraude en pagos cripto?
Intentos de que salga mercancía, crédito o un pago por dinero que nunca se liquidó contra el pedido. Sobre una vía blockchain la reversión no existe, así que el intento tiene que aterrizar antes del envío y no semanas después.
¿Un hash de transacción prueba que una factura se pagó?
No. Un hash prueba que existe una transferencia en una cadena. Si llegó a la dirección de esta factura, en un activo aceptado, antes de cerrarse la ventana y a la profundidad de confirmación exigida es lo que contesta el estado de la factura.
¿Por qué entró dinero sin que el pedido quedara pagado?
Porque llegó a la dirección sin pagar la factura: con la ventana ya cerrada, contra una factura pagada o cancelada, o en otro activo. El saldo lo recibe menos la comisión de procesamiento, la factura se queda donde estaba y no sale ningún webhook.
¿Se puede falsificar un webhook de pago?
Cualquiera puede hacer POST a una URL de notificación, y por eso cada entrega lleva una firma HMAC-SHA256 sobre el cuerpo en bruto. Verifícala con una comparación en tiempo constante, lee los valores `v1` de la cabecera como una lista y comprueba la marca temporal: la firma sola no frena un reenvío.
¿Paymos exige 2FA para añadir una dirección de retiro?
Solo si ese usuario del comercio tiene TOTP o una passkey activados. La verificación reforzada refuerza un segundo factor que ya existe, así que a quien no lo tiene no se le pregunta. La lista blanca sí actúa siempre, y una lista vacía desactiva los retiros.
¿Cómo evito que un webhook duplicado envíe el pedido dos veces?
Descarta entregas por el id `evt_` de `X-Webhook-Id` y apunta el pedido contra el id de factura que viaja dentro de `data`. Usar el id de entrega como clave del pedido hace que dos endpoints apunten dos pagos.
Cuándo NO conviene usar un filtro antifraude propio
- Si el dinero llegó en otro activo o con la ventana ya cerrada, eso es conciliación y no un caso de fraude. La página de saldos ya trae el motivo escrito; un modelo de puntuación no añade nada a un dato que puedes leer.
- Si vendes a clientes con nombre y con condiciones pactadas, la identidad ya es tu control. Un filtro por delante te produce sobre todo una cola de pedidos que ibas a servir igual.
- Si tus precios viven en el servidor y no envías hasta tener un estado final de factura verificado, los siete intentos de esta página ya están cerrados. Puntuar una puerta cerrada solo te compra falsos positivos.
- Si lo que te preocupa es que te pague una billetera robada, nada de tu lado lo ve. La cadena confirma que una clave firmó; si su dueño quería hacerlo, eso no lo dice nadie.
Fuentes
- 1. EIP-20 — Token Standard (accessed 2026-09-15)
- 2. CWE-602 — Client-Side Enforcement of Server-Side Security (accessed 2026-09-15)
- 3. CWE-345 — Insufficient Verification of Data Authenticity (accessed 2026-09-15)
- 4. W3C — Web Authentication: An API for accessing Public Key Credentials Level 2 (accessed 2026-09-15)
- 5. Paymos — verificar la firma de un webhook (accessed 2026-09-15)
- 6. Paymos — seguridad (accessed 2026-09-15)
Última revisión: 15 sept 2026


