Pentest

Seguridad de APIs: los fallos que más se repiten

Cuando se revisa una API ajena, los problemas casi nunca están en la criptografía ni en algún truco exótico. Están en lo cotidiano: un endpoint que confía en un identificador que manda el cliente, una respuesta que devuelve el objeto entero de la base, una ruta vieja que nadie recordaba. Lo que sigue es una lista para recorrer antes de publicar, y para volver a recorrer cada tanto. Todo lo que aparece acá se prueba únicamente sobre sistemas propios o con autorización escrita.

Autorización por objeto

Es la falla más repetida. El usuario está autenticado, la ruta es GET /pedidos/1042, y el servidor devuelve el pedido sin preguntarse si pertenece a quien lo pide. Si cambiando el número aparece el pedido de otra persona, hay un problema de autorización a nivel de objeto.

La defensa es mecánica: cada acceso a un recurso tiene que verificar que el usuario autenticado puede ver o modificar ese recurso puntual. Lo más sano es que el filtro por dueño viva en la capa de datos y no en cada controlador, para que no dependa de que alguien se acuerde.

def obtener_pedido(usuario, pedido_id):
    pedido = db.pedidos.get(id=pedido_id, cuenta_id=usuario.cuenta_id)
    if pedido is None:
        raise NoEncontrado()  # mismo error exista o no
    return pedido

Devolver el mismo error cuando el recurso no existe y cuando no es tuyo evita que alguien use la API para averiguar qué identificadores son válidos.

Autorización por propiedad y datos de más

Hay una variante más fina. El usuario puede ver su perfil, pero ¿puede modificar el campo rol? Si el endpoint de actualización vuelca el cuerpo de la petición directamente sobre el modelo, un cliente curioso agrega "rol": "admin" y listo. La salida es una lista explícita de campos editables por operación, no una lista de campos prohibidos.

Del otro lado ocurre lo mismo con las respuestas. Es común serializar el objeto completo y confiar en que el frontend muestre solo lo necesario. El navegador igual recibe el hash de la contraseña, el correo de otros usuarios o banderas internas. Definí un esquema de salida por endpoint y devolvé solo lo que ese consumidor necesita.

Límites: de tasa y de tamaño

Una API sin límites invita a la fuerza bruta contra el login, al scraping masivo y a que un cliente mal escrito te tire el servicio por accidente. Poné límites de tasa por usuario, por IP y por endpoint, con más rigor en los sensibles (login, recuperación de contraseña, envío de códigos).

Los límites de tamaño se olvidan más seguido. Acotá el largo de los cuerpos, la cantidad de elementos de una lista, la profundidad de un JSON y el máximo de resultados por página. Un parámetro limite=1000000 no debería ser aceptado solo porque es un número válido.

Validación de entradas

Validá tipo, formato, rango y largo en el borde, con un esquema, y rechazá lo que no encaja en lugar de intentar arreglarlo. Esto sirve contra inyecciones, pero también contra errores comunes: un entero negativo en una cantidad, una fecha imposible, un arreglo donde esperabas un texto. La validación no reemplaza a las consultas parametrizadas ni al escapado de salida; son capas distintas.

Inventario y versiones viejas

Casi toda API acumula historia. Existe /v1, /v2, un endpoint de pruebas que quedó, una ruta de administración pensada para uso interno. Las versiones viejas suelen tener los controles de autorización que se les ocurrieron en su momento, no los de ahora. Llevá un inventario real de lo que está expuesto, apagá lo que ya no se usa y generá la documentación desde el código para que no se desfase. Si tenés un gateway, comparar sus rutas contra las que el código declara destapa sorpresas.

Servicio a servicio

Que una llamada venga de la red interna no la vuelve confiable. Los servicios tienen que autenticarse entre sí, con credenciales propias de cada uno, permisos acotados a lo que necesitan y posibilidad de rotarlas. Un servicio que acepta cualquier petición porque "solo lo llaman los nuestros" se convierte en un trampolín en cuanto otro componente se compromete.

Errores que no cuentan de más

Los mensajes de error son una fuente de información. Una traza de pila, el texto de una consulta SQL o la diferencia entre "usuario inexistente" y "contraseña incorrecta" le ahorran trabajo a quien investiga. Devolvé mensajes genéricos al cliente, con un código de correlación, y dejá el detalle en los registros del servidor, donde además conviene anotar los accesos denegados: una ráfaga de 403 o 404 sobre identificadores consecutivos es una señal que vale la pena alertar.

Una buena forma de cerrar la revisión es elegir tres endpoints al azar y preguntarte, para cada uno, quién puede llamarlo, qué objetos puede tocar y qué campos ve de vuelta. Si alguna de esas respuestas lleva más de un minuto, ahí hay algo para ordenar.