Pentest

Control de acceso roto: cuando un usuario ve lo que no debería

Un usuario inicia sesión, abre su factura y ve en la barra de direcciones algo como /facturas/1042. Cambia el número por 1043 y aparece la factura de otra persona. No hubo ninguna técnica sofisticada, solo un servidor que verificó quién era el usuario pero no si el recurso le pertenecía. Es de las fallas más comunes porque el sistema funciona perfectamente para quien lo usa como se espera.

Autenticación y autorización

Son dos preguntas distintas. La autenticación responde quién sos; la autorización responde qué podés hacer. Muchas aplicaciones resuelven bien la primera y dejan la segunda librada a que cada endpoint se acuerde de comprobarla. Con cien endpoints, alguno se va a olvidar.

Conviene separar dos niveles:

El caso de manual: IDOR

La referencia directa e insegura a objetos ocurre cuando la aplicación usa un identificador recibido del cliente para buscar un recurso sin verificar la propiedad. Código vulnerable:

# VULNERABLE: cualquier usuario autenticado lee cualquier factura
@app.get("/facturas/<int:factura_id>")
def ver_factura(factura_id):
    return db.facturas.obtener(factura_id)

Versión corregida, donde la consulta ya incluye al dueño:

@app.get("/facturas/<int:factura_id>")
def ver_factura(factura_id):
    factura = db.facturas.obtener(factura_id, usuario_id=usuario_actual().id)
    if factura is None:
        abort(404)
    return factura

Fijate dos detalles. La pertenencia se comprueba en el servidor, dentro de la propia búsqueda, así que no hay un paso intermedio que se pueda saltear. Y si el registro existe pero es de otro, la respuesta es la misma que si no existiera, para no confirmar qué identificadores son reales.

El mismo problema aparece en actualizaciones, borrados, descargas de archivos y en identificadores que viajan en el cuerpo de la petición o en cabeceras, además de la URL.

No confiar en el frontend

Ocultar un botón de "eliminar usuario" a quien no es administrador mejora la experiencia, pero no es un control de seguridad. Cualquiera puede armar la petición a mano, sin pasar por la interfaz. Toda decisión de permisos tiene que repetirse en el servidor. Lo mismo vale para datos que el cliente "informa" sobre sí mismo: un campo oculto con el rol, un identificador de usuario en el cuerpo, un precio. Lo que el servidor sabe de la sesión manda sobre lo que el cliente diga.

Centralizar para no depender de la memoria

El objetivo de diseño es que hacer lo correcto sea el camino por defecto y que olvidarse sea difícil. Algunas tácticas:

  1. Denegar por defecto. Una ruta nueva sin regla explícita no debería ser accesible.
  2. Una sola capa de autorización, ya sea un middleware, decoradores o una política por recurso, en vez de condicionales dispersos en cada controlador.
  3. Roles y permisos definidos en un lugar, con nombres entendibles, para poder auditar quién puede qué sin leer todo el código.
  4. Consultas que siempre filtran por dueño o por organización, por ejemplo mediante un repositorio que exija el contexto del usuario como parámetro. Si es obligatorio pasarlo, no se puede olvidar.
  5. Mínimo privilegio, con permisos asignados según la tarea y revisados cuando alguien cambia de rol.

En sistemas con varias organizaciones en la misma base, el aislamiento entre inquilinos merece atención especial, porque un descuido filtra datos entre clientes.

Probarlo automáticamente

Las revisiones manuales no escalan. Escribí pruebas automáticas que creen dos usuarios con recursos propios y verifiquen que el primero no puede leer, modificar ni borrar los del segundo, para cada endpoint que reciba un identificador. Agregá casos por rol: que un usuario común reciba rechazo en cada ruta administrativa. Si tu framework puede listar las rutas, una prueba que falle cuando aparece una ruta sin regla de acceso asociada detecta olvidos antes de producción.

También ayuda registrar los rechazos de autorización y revisar los patrones: alguien que recorre identificadores consecutivos deja una huella clara.

Identificadores impredecibles

Usar identificadores aleatorios y largos en lugar de números consecutivos hace más difícil adivinar recursos ajenos. Es un buen complemento, pero no una defensa: si un identificador se filtra en un correo, un registro o un enlace compartido, la verificación de propiedad sigue siendo lo único que protege el dato. Primero la comprobación en el servidor; el identificador opaco, después, como capa adicional.