Diario de una auditoría en tiempo real

Escribo esto de madrugada con el ventilador zumbando de fondo y mi gato Bianchi durmiendo encima de unos cables. Son casi las tres de la mañana aquí en Barinas. Llevo un par de semanas metido en un juego del gato y el ratón que me quitó el sueño por completo. Todo empezó a principios de agosto cuando me crucé con el lanzamiento de CabildoOS.
El proyecto proponía una plataforma de democracia digital para Venezuela. Prometían participación ciudadana, identidades verificadas y voto secreto.
Sonaba demasiado bien.
Construir algo así en nuestro contexto político es caminar sobre un campo minado. Si te equivocas en la infraestructura, no solo tumban el sitio. Pones en riesgo a gente real. Esa idea me dio vueltas en la cabeza todo el día. Decidí abrir mi terminal en Debian, levantar un par de contenedores en Docker para aislar mi entorno y tirar unas cuantas peticiones para ver cómo respiraba el sistema.
No buscaba dañar nada. Quería entender si la promesa de anonimato aguantaba un escrutinio básico.
Lo que encontré en las primeras horas me dejó frío.
El archivo expuesto y la puerta abierta
Mi rutina de reconocimiento siempre arranca igual. Miro cabeceras, registros DNS, busco rutas comunes. El sitio principal estaba escondido detrás de Cloudflare con un cartel de "Próximamente". Se me ocurrió probar rutas de desarrollo.
Lancé una petición a la ruta del archivo de entorno y el servidor me escupió un código 200. No me devolvió un archivo de texto plano. Me devolvió la aplicación completa de producción, el bundle entero en JavaScript de más de 600 kilobytes.
Tener el frontend expuesto te permite leer el código fuente. Analicé el archivo y encontré la configuración de conexión a Supabase. Extraje la clave pública JWT, esa que usan los usuarios no autenticados para leer datos básicos.
Fui a mi consola, armé un request contra la API de PostgREST y pedí el esquema de la base de datos.
La respuesta fue un JSON enorme. Tenía acceso de lectura a casi todo.
Encontré una función remota llamada set_system_config. Mandé un POST con un payload de prueba, esperando que el servidor me rebotara con un error de permisos. La terminal parpadeó y me devolvió un HTTP 204.
Éxito.
Acababa de escribir en la configuración global del sistema sin estar logueado. Podía definir mi propio código de acceso a la beta. Podía apagar la plataforma entera activando el modo mantenimiento. Tenía control total sobre el estado de la aplicación.
Eso era malo, pero no era lo peor. Fui a revisar la tabla de votos.
El sistema almacenaba las papeletas. Cada registro decía si la persona votó a favor o en contra. Al lado del voto había una columna llamada seat_number. El número de butaca. Fui a la tabla de perfiles, que también era pública, y vi que cada usuario real tenía asignado su número de butaca.
Hice la conexión en mi cabeza. El asiento 1 era el usuario con el alias "martinlevar". El asiento 1 tenía 25 votos registrados. Podía leer exactamente qué votó "martinlevar" en cada una de las propuestas.
El anonimato no existía. En un país donde las listas de persecución política destruyeron carreras y vidas enteras, esto era una bomba de tiempo. Para quienes leen esto desde otros países, o para los que tienen memoria corta, hay que hacer un paréntesis obligado:
En el año 2004, un diputado llamado Luis Tascón publicó en internet los datos exactos de más de tres millones de venezolanos. Eran las personas que firmaron para solicitar un referéndum revocatorio contra el gobierno. Yo ni siquiera había nacido cuando eso pasó, llegué al mundo un par de años después, pero crecí respirando las consecuencias de ese desastre en cada reunión familiar y en los pasillos de mi ciudad.
Aparecer en ese documento significaba la muerte civil. La gente perdía su trabajo en PDVSA o en los ministerios de la noche a la mañana. De pronto, tramitar un simple pasaporte se volvía un callejón sin salida. Convirtieron un registro electoral en una guillotina administrativa.
Por eso la pantalla de mi terminal me daba tantas náuseas. Ver una tabla relacional en pleno 2026 que vuelve a atar un nombre propio a una postura política me revolvió el estómago. No encontré un error que te clona la tarjeta de crédito. Me topé con el código fuente para armar una nueva cacería de brujas. Tenía frente a mí la Lista Tascón 2.0, lista para empaquetar y descargar.
Redacté un correo esa misma noche. Busqué ser lo más claro posible. Les expliqué el riesgo del voto reidentificable y la función administrativa expuesta. Traté de no sonar como una amenaza. Quería que entendieran que estaba de su lado.
La matemática de la privacidad
El equipo reaccionó rápido. A las 48 horas me confirmaron que habían aplicado los parches. Entré de nuevo a verificar.
Habían blindado la función de configuración. Ya no podía escribir nada. Revisé la tabla de votos y vi que borraron la columna del número de butaca. El voto en sí mismo flotaba ahora en el vacío, sin dueño. Me pareció una solución elegante. Bloquearon también el acceso público a los perfiles de los usuarios.
Parecía que el problema estaba resuelto. Me quedé mirando los datos un rato largo. Habían implementado una tabla nueva para registrar la asistencia. La llamaron vote_seats. Te decía qué butaca participó en qué pregunta, pero no decía qué votó.
Empecé a jugar con los números. Tomé una pregunta real sobre una figura política. Consulté la tabla de asistencia. El servidor me dijo que solo la butaca número dos había participado en esa consulta específica. Fui a la función que sumaba los votos totales de esa pregunta. El servidor me devolvió un solo voto a favor.
Acababa de desanonimizar a la butaca número dos.
Si sabes que una sola persona entró al cuarto oscuro, y luego ves que salió un solo voto positivo de la urna, sabes qué votó esa persona. La lógica es aplastante. Escribí un script en Python al que llamé reidentification_analyzer.py.
Le programé una rutina para buscar votaciones con censo reducido. Toda pregunta con un solo votante era una fuga de identidad determinista. La matemática no perdona los grupos pequeños. Les avisé que necesitaban implementar un umbral.
Si no hay al menos cinco personas votando, el sistema no debería mostrar los resultados parciales.
Mientras analizaba esto, encontré otro error. El registro de usuarios en Supabase Auth seguía abierto. Cualquiera podía mandar un POST directo a la API y crearse una cuenta sin necesidad de tener el código de invitación beta. La barrera solo existía visualmente en el frontend. Creé un correo temporal, mandé la petición y el sistema me dio la bienvenida. Ahora tenía una sesión autenticada válida para seguir explorando.
El silencio y la refactorización
Pasaron varios días sin actualizaciones. Aproveché ese tiempo para construir un pequeño arsenal. Programé una consola SQL interactiva que renovaba mis tokens automáticamente y traducía mis comandos a formato PostgREST. Quería estar listo para cuando movieran las piezas.
El 21 de agosto me contactaron.
Habían reconstruido gran parte del sistema. Migraron la verificación de identidad a un entorno separado usando Cloudflare Workers. Me pusieron en una lista blanca en su firewall para que mi IP pudiera saltarse los bloqueos y auditar la nueva versión 2.0.
La arquitectura había cambiado por completo. El backend antiguo desapareció. Ahora todo el peso de analizar los documentos de identidad recaía sobre un servicio nuevo conectado al modelo Gemini Vision.
Decidí probar la resiliencia del modelo de inteligencia artificial. Preparé una imagen PNG sintética de un solo píxel codificada en base64. Si el sistema era frágil, intentar procesar un píxel como si fuera una cédula de identidad lo haría colapsar o consumiría un tiempo de cómputo absurdo. Disparé el payload.
El servidor me rechazó la petición en 320 milisegundos.
El mecanismo de fail-fast estaba perfectamente afinado. Intenté saltarme la verificación enviando la imagen del píxel directamente al endpoint de prueba de vida. El sistema me frenó en seco exigiendo un token de sesión criptográfico que solo te daban si pasabas el paso anterior. Estaba frente a una muralla muy bien pensada.
Reconozco un buen código cuando lo veo.
El eslabón más débil
Dejé el worker de verificación y me moví al subdominio de la API principal. Estaba construida en FastAPI.
Hice un escaneo de rutas y encontré la documentación de OpenAPI completamente pública. Leí el mapa del tesoro. Había una ruta llamada /api/admin/users.
Normalmente los desarrolladores protegen estas rutas exigiendo un token de sesión. Pensé en mi cuenta de prueba, la que creé saltándome el código beta días atrás. Era un usuario regular, sin privilegios, una cuenta de cartón.
Escribí el script admin_bfla_audit.py. Le pasé mis credenciales básicas, armé la cabecera con mi Bearer token y lancé una petición GET contra la ruta de administradores.
Esperaba un 403 Forbidden. Esperaba un mensaje diciendo que mi rol no era suficiente.
La pantalla se llenó de texto. Un código 200 OK.
El servidor me devolvió el censo completo de la plataforma. Vi los identificadores únicos, los correos electrónicos reales, los alias, las fechas de conexión y el estado de los documentos de cada persona registrada.
El backend verificaba que el token fuera válido y que perteneciera a un usuario logueado. Olvidaron decirle al sistema que verificara si ese usuario tenía permisos de administrador. Es lo que llamamos Broken Function Level Authorization. Una falla de concepto. Puedes construir el muro criptográfico más alto del mundo alrededor de tu base de datos, pero si el guardia de la puerta trasera deja pasar a cualquiera que tenga una identificación genérica, perdiste el juego.
Con esa lista de correos en mi poder, podía cruzar los alias con los números de butaca del hemiciclo virtual. Y con los números de butaca, volver a aplicar la deducción matemática en las votaciones pequeñas.
El anonimato volvía a romperse, esta vez desde un ángulo completamente distinto.
Empaqueté los registros, los resultados de los scripts y las capturas de pantalla. Armé el reporte final de cierre. Les recomendé implementar un control de acceso por roles estricto en los controladores de FastAPI y ocultar la documentación de la API en producción.
Cierro la terminal y me quedo pensando en lo frágil que es el software. Las aplicaciones son organismos vivos. Parcheas una arteria y empieza a sangrar una vena en el otro extremo del sistema.
Auditar no se trata de destruir el trabajo de otro desarrollador para sentirte superior. Trata de encontrar esas fallas conceptuales antes de que alguien con intenciones reales de hacer daño las use como arma.
El equipo de CabildoOS tiene un reto enorme por delante. Construyen algo necesario bajo condiciones que no perdonan errores. Esta noche rompí su sistema de autorización. Mañana ellos lo arreglarán y el código será un poco más fuerte. Así funciona esto. Y supongo que de eso se trata la ingeniería.
Me voy a dormir. El sol está a punto de salir.



