Saltar a contenido

Antifraude y seguridad

Un sorteo con premios reales atrae a quien quiere ganarle al sistema, y un padrón de clientes es un dato sensible que hay que cuidar. El Sistema de Premios trata estas dos cosas —evitar el fraude y proteger los datos— como requisitos de primera clase, no como un agregado. Esta página explica las defensas: qué señales delatan un abuso, qué límites frenan a un atacante, y qué invariantes de seguridad no se rompen por ninguna conveniencia. La base normativa son los §8 y §9 del SPEC y la CONSTITUTION del proyecto, que define lo nunca aceptable.

Antifraude: las señales que delatan el abuso

El fraude en un sorteo por vouchers tiene patrones reconocibles. El panel del gestor expone señales de concentración que hacen visible lo que a simple vista se perdería:

Señal Qué delata
Concentración por DNI Muchos vouchers apuntando a un mismo DNI: alguien acumulando chances de forma anómala.
Concentración por conexión Muchas participaciones desde una misma conexión: un bot o una granja operando desde un punto.
Concentración por caja Vouchers concentrados en una caja puntual: posible emisión irregular o una caja comprometida.
Firma inválida dispersa Muchos rechazos por firma inválida con origen disperso: alguien probando tokens fabricados a ver si alguno pega.

Estas señales se alimentan del registro interno de rechazos (VoucherRechazado), donde queda el motivo real de cada intento fallido. Ninguno de esos motivos viaja al cliente: hacia afuera, todo rechazo es genérico (ver más abajo).

Los límites que frenan el abuso en caliente

Las señales son para observar; los límites son para frenar. Hay tres, y operan en momentos distintos del flujo:

  • Rate limit por conexión. El propio sistema aplica límites de frecuencia por conexión, para que un bot no pueda martillar el endpoint de participación. Se aplica sobre /api/**, así que cubre también los caminos de reconocimiento del dispositivo.
  • Verificación anti-robots invisible (honeypot). El envío del formulario lleva una verificación anti-robots invisible para el cliente legítimo: no hay CAPTCHA que resolver, no hay fricción. Un bot que llena todos los campos cae en la trampa; una persona real ni se entera de que existe.
  • Tope diario por DNI. El máximo de chances por DNI por día de emisión (5 en esta campaña). El voucher que lo supera se rechaza sin consumirse y se informa. Al resolverse por día de emisión, el rechazo es definitivo para ese día: no hay "reintentá mañana". Ver ADR-006 y Los casos de participación.

El tope no le quema la chance a nadie

El tope diario rechaza sin consumir el voucher: un cliente legítimo que compró mucho un día no pierde sus vouchers, simplemente no puede convertir más de 5 en chances de ese día de emisión. La defensa antifraude no castiga al cliente honesto.

Seguridad del token: la firma y su secreto

El token es la única superficie de contacto con las cajas, así que su integridad es crítica. Las defensas están pensadas para que un atacante no pueda fabricar vouchers ni aprender del sistema probándolo.

  • Comparación de la firma en tiempo constante. El HMAC del token se compara en tiempo constante, nunca con un == byte a byte que termine antes al primer byte distinto. Un == ingenuo filtraría, por timing, cuánto de la firma acertó un atacante, dándole una vía para forjar una firma válida a fuerza de medir. Tiempo constante cierra ese canal (CWE-208).
  • El secreto HMAC, cifrado at-rest. El secreto de firma vive cifrado (master key vía variable de entorno / Vaultwarden), nunca en claro en el repositorio ni en los logs. El "keystore" es una tabla clave_firma cifrada, no un archivo suelto. Ver ADR-010.
  • Rotación de clave sin corte. Como el token lleva su keyId, se puede rotar la clave dando de alta una nueva y validando ambas durante la transición. Si una caja se compromete, se rota el keyId: es un riesgo aceptado y documentado, mitigado por la rotación más las señales de concentración por caja.

Errores genéricos hacia afuera: no darle un oráculo al atacante

Hacia el cliente, todos los rechazos se parecen: un error genérico, sin decir si fue firma inválida, token ya usado o promo vencida. El motivo real queda solo en el registro interno. Si el sistema distinguiera los errores, le estaría enseñando a un atacante a separar tokens bien formados de basura, o a mapear el estado de vouchers ajenos. Es un invariante de la CONSTITUTION.

Seguridad de los datos: qué se guarda y qué no

El principio rector es que una filtración de la base no debe permitir reconstruir nada útil. De ahí varias decisiones:

  • En la base vive el hash del token, nunca el token. Quien acceda a la DB no puede reconstruir direcciones de participación válidas (/p/{token}) a partir de los hashes. Mismo criterio, misma protección (CWE-312).
  • El DNI nunca se persiste en el navegador. El reconocimiento del dispositivo —el "Hola {nombre}" de la segunda participación— no guarda el DNI en localStorage, ni siquiera un hash del DNI (un DNI son ~90 millones de valores, un hash se revierte por fuerza bruta). Usa un secreto opaco emitido por el backend que no significa nada sin el servidor, y en la base se persiste solo el hash de ese secreto. Ver ADR-027.
  • El reconocimiento no filtra datos ni autoriza. El endpoint de reconocimiento devuelve solo primer nombre + DNI enmascarado, nunca el DNI completo ni contacto. Y reconocer no habilita a editar datos: modificar un cliente exige haber presentado el DNI en la sesión. El teléfono puede ser compartido (la caja del súper, un familiar), así que reconocer nunca implica participar por otro: siempre hay confirmación explícita, y "No soy yo" borra el secreto.

Seguridad de la aplicación

Las defensas transversales que aplican a todo el backend:

  • SQL siempre parametrizado, nunca concatenado (CWE-89): ninguna entrada de usuario se pega dentro de una query.
  • Toda entrada externa se valida antes de usarse (CWE-20).
  • Sin secretos en logs ni en el repositorio (CWE-532 / CWE-798): las credenciales de la DB y la master key van solo por variables de entorno.
  • La configuración operativa vive fuera del jar y el arranque falla rápido si falta, en vez de arrancar con defaults invisibles. El archivo de configuración versionado jamás contiene un valor secreto, solo referencias ${SP_*}. Ver ADR-029.

Auditoría: la evidencia que se captura, no se narra

La última capa de seguridad es la trazabilidad. El sistema no "cuenta" lo que hizo: lo registra para que sea verificable.

  • Actas reproducibles. Cada sorteo deja un snapshot inmutable que reproduce el resultado exacto (lista + seed + algo@version → mismo resultado). Un reclamo se responde reproduciendo, no argumentando. Ver Motor de sorteo y acta y ADR-004.
  • Eventos de premio. Cada cambio de estado de una unidad queda auditado con quién, cuándo y por qué (EventoPremio). Ver Premios y su ciclo de vida.
  • Cambios de datos del cliente auditados. Toda corrección de datos de un cliente queda registrada (quién por DNI, qué campo, valor anterior → nuevo, cuándo) en el registro interno, nunca expuesta por la API pública.
  • Contactos con ganadores registrados. Los intentos de contacto quedan asentados como prueba frente al plazo de retiro de las bases.

Evidencia por ejecución, nunca narrada

Es un principio de la CONSTITUTION: la evidencia se captura por la ejecución del sistema, no se relata. Un acta que se reproduce, un evento que registra una transición, un contacto asentado con fecha: todo es verificable de forma independiente. Esa es la diferencia entre "confiá en nosotros" y "mirá el registro".

Dónde sigue