BookinglyTech News
Ciberseguridad

Neon Data API y RLS: 27 ataques rechazados y una brecha de quince minutos

Un tablero de tareas multiinquilino sin backend resiste 27 intentos de intrusión apoyándose solo en Postgres. El fallo que no pudieron tapar las políticas fue el tiempo de vida del token.

3 min de lecturaDev.to0 vistas

Un equipo montó un tablero de tareas multiinquilino sin backend: el navegador carga ficheros estáticos, se autentica con Neon Auth y lee y escribe a través de la Data API de Neon. No hay servidor de API, ni función serverless, ni middleware. Quién pertenece a cada equipo es cosa de Neon Auth; qué puede leer o escribir un usuario autenticado lo decide Postgres, con políticas de row-level security, permisos de columna y una función escrita a mano.

Con eso montado, dieron a la propietaria de un segundo equipo una cuenta válida y un script, y la dejaron intentar 25 vías de entrada al primer equipo, más dos intentos desde dentro con el rol equivocado. Filtros fabricados, joins embebidos, recuentos agregados, tokens falsificados, un algoritmo alg:none, un token firmado con su propia clave, actualizaciones masivas, upserts sobre identificadores del otro equipo. Las 27 fueron rechazadas. Cada intento tenía que devolver exactamente la respuesta esperada y cada columna de las filas de la víctima se comparaba antes y después, de modo que un rechazo que hubiera tocado datos en silencio habría cantado.

La muralla y el hueco que queda

El aislamiento se apoya en un claim firmado. El equipo activo viaja en el JWT como organización, auth.organization_id() lo lee dentro de Postgres y cada política compara contra él. El tenant de las tablas toma ese valor por defecto. La clave primaria es compuesta, (org_id, id), para que un insert fallido no delate que un identificador existe en otro equipo.

Los permisos de columna son la segunda pared. Una política con WITH CHECK (true) no dejó pasar nada por sí sola, porque al cliente nunca se le concede la columna org_id. Lo que abrió la puerta fue añadir permisos a nivel de tabla por encima. El equipo rompió su propia capa de seguridad de cuatro formas realistas, de una en una: tres dejaron pasar ataques concretos y la batería los detectó; la cuarta no hizo nada hasta que se sumó el permiso de tabla.

Lo que las políticas no pudieron parar fue el reloj. Un miembro expulsado de un equipo siguió leyendo sus datos durante quince minutos y medio, la vida del token que ya tenía en la mano más unos treinta segundos. La corrección es consultar la tabla de miembros de Neon Auth dentro de las políticas, y corta el acceso de inmediato.

Qué cuesta operar esto

En la prueba de latencia, el coste mediano fue inferior a un milisegundo para lecturas y escrituras, y de unos 5 ms para la llamada RPC. Añadir la comprobación de membresía mantuvo lecturas y escrituras dentro del milisegundo y encareció el RPC en esos 5 ms. Una petición a la Data API tardaba entre 41 y 44 ms en el percentil 50, mientras que un connect TCP plano a la misma dirección costaba 40 ms. Hace falta Node.js 20 o superior, un proyecto de Neon con Neon Auth y la Data API aprovisionada en la misma rama.

El interés de esto no es que un tablero de tareas sea seguro, sino que el modelo de autorización completo cabe en dos ficheros SQL. Cuando la lógica se va del código propio a la base de datos, lo que antes era un bug en un servidor pasa a ser una política mal escrita, y la revocación de sesión ya no la controla tu aplicación: la controla el tiempo de vida del token. Quien vaya a adoptar este patrón debería medir ese intervalo antes que nada. El repositorio con la suite de ataques y el esquema está en GitHub.