¿Está sufriendo un ataque? Solicite respuesta a incidentes

[email protected]
Seguridad web

Pruebe cada ID de su aplicación frente a IDOR y BOLA

Cómo hallar endpoints que devuelven registros ajenos al cambiar un ID, corregirlos con consultas acotadas y qué revisar en una revisión de código.

También en:EnglishTürkçeDeutschFrançaisItalianoPolskiNederlandsEesti

Ilustración: Pruebe cada ID de su aplicación frente a IDOR y BOLA

Una referencia directa insegura a objetos (IDOR, por sus siglas en inglés) existe allí donde su aplicación acepta un identificador del cliente y devuelve o modifica el registro correspondiente sin preguntarse si ese usuario puede tocarlo. En el mundo de las API se conoce el mismo fallo como autorización rota a nivel de objeto (BOLA), y encabeza la lista OWASP API Security de 2023. Para prevenirlo hace falta una comprobación de autorización en cada acceso a un objeto, aplicada en toda la aplicación y cubierta por pruebas.

De qué se compone un IDOR y dónde se esconde

La hoja de referencia de OWASP sobre IDOR divide un IDOR en tres partes: un objeto (una cuenta, un documento, un ticket, una transacción), una referencia a él (un ID, un UUID, un número de cuenta, un token o un slug) y la ausencia de una comprobación a nivel de objeto. La tercera parte es el fallo.

La referencia no siempre es /users/123 en la URL. Puede estar en un campo oculto de un formulario como user_id, en una query string, en una cabecera de la petición o en un payload JSON. También puede ser un nombre de fichero: si GET /documents/annual-report.pdf funciona, pruebe con el nombre de otro documento válido.

La entrada de OWASP API Security sobre BOLA valora la explotabilidad como fácil, la prevalencia como generalizada y la detectabilidad como fácil. Los servidores de API normalmente no guardan el estado del cliente, así que dependen de los ID que este envía para decidir qué objeto cargar. Sus escenarios de ejemplo:

  • Un endpoint de analítica de tiendas con la forma /shops/{shopName}/revenue_data.json, combinado con un segundo endpoint que lista todos los nombres de tienda, expone los datos de ventas de miles de comercios a un simple script.
  • Una API de vehículos que acepta un VIN pero nunca comprueba que el vehículo pertenezca al usuario autenticado.
  • Una mutación GraphQL deleteReports que elimina cualquier ID de documento que reciba.

El último caso es un borrado, así que pruebe las actualizaciones y los borrados con el mismo cuidado que la página que muestra el registro.

Pruebe con dos cuentas

Cree un Usuario A y un Usuario B, dé a cada uno sus propios objetos (documentos, tickets, facturas), autentíquese como A y repita las peticiones con los identificadores de B.

  1. Cree dos cuentas con ámbitos de autorización distintos.
  2. Cree objetos propiedad de cada usuario.
  3. Inicie sesión como Usuario A y capture todas las peticiones que lleven una referencia a un objeto.
  4. Sustituya por las referencias del Usuario B en la ruta, la query, el cuerpo y las cabeceras.
  5. Confirme que la aplicación deniega el acceso en todos los casos.

Cubra las acciones de lectura, creación, actualización, borrado, exportación y administración. No omita un endpoint porque sus identificadores parezcan aleatorios. La hoja de referencia indica que el acceso debe denegarse tanto si el identificador es predecible como si es imposible de adivinar.

Conviértalo en una prueba de regresión

Cuando la pasada manual funcione, conviértala en código. Un esbozo en pytest, que supone fixtures que le proporcionan una sesión autenticada del Usuario A y el ID de un documento propiedad de B. Cada petición debe ser válida en todo lo demás, con un cuerpo bien formado, para que un 400 o un 415 de la validación de entrada no haga pasar la prueba por un motivo equivocado:

import os
import pytest

BASE_URL = os.environ["APP_BASE_URL"]  # from your test config
DENIED = 404  # your chosen response for foreign IDs

ENDPOINTS = [
    ("GET", "/documents/{id}", None),
    ("PUT", "/documents/{id}", {"title": "changed"}),
    ("DELETE", "/documents/{id}", None),
    ("GET", "/documents/{id}/export", None),
    ("POST", "/documents/{id}/comments", {"text": "hello"}),
]

@pytest.mark.parametrize("method,path,body", ENDPOINTS)
def test_a_cannot_touch_bs_document(method, path, body, session_a, b_document_id):
    url = BASE_URL + path.format(id=b_document_id)
    r = session_a.request(method, url, json=body)
    assert r.status_code == DENIED

Decida de forma deliberada entre 403 y 404. Un 403 ante un ID ajeno confirma que ese ID existe, lo que ayuda a un atacante a enumerar. Elija el que elija, verifique únicamente ese código. Genere la lista de endpoints a partir de su tabla de rutas para que los nuevos endpoints aparezcan automáticamente, y bloquee los despliegues cuando la batería de pruebas falle.

GraphQL requiere dos casos adicionales. Consulte un ID ajeno en todos los niveles del árbol para ver si los resolvers anidados repiten la comprobación del padre. En las operaciones por lotes, como el array reportKeys mencionado arriba, envíe una petición que mezcle sus propios ID con los de otra persona y confirme que toda la llamada falla o que el elemento ajeno se omite.

Acote la búsqueda al usuario

Compruebe la autorización de cada objeto al que un usuario intente acceder, con el mecanismo que ofrezca su framework, de modo que la comprobación sea estructural y no algo que cada desarrollador deba recordar. El patrón más directo consiste en buscar los objetos dentro del conjunto al que el usuario actual tiene acceso. La versión en Rails:

# vulnerable, searches all projects
@project = Project.find(params[:id])

# secure, searches projects related to the current user
@project = @current_user.projects.find(params[:id])

El handler de Spring Boot de la hoja de referencia llama a findById(id) y por tanto devuelve cualquier documento a quien pueda adivinar u obtener un ID. El fragmento siguiente es una ilustración nuestra, no código de OWASP. Supone una consulta derivada de Spring Data sobre un campo ownerId, y currentUser() representa su helper que lee el contexto de autenticación de confianza:

@GetMapping("/documents/{id}")
public Document getDocument(@PathVariable Long id) {
    return documentRepository
        .findByIdAndOwnerId(id, currentUser().getId())
        .orElseThrow();
}

Siempre que pueda, evite exponer identificadores y obtenga el usuario a partir de la sesión. En los flujos de varios pasos, conserve los identificadores en la sesión para que el cliente no pueda manipularlos entre un paso y otro.

Los identificadores aleatorios ayudan, pero solo como defensa en profundidad. Añada una columna con una cadena aleatoria y úsela en las URL, o utilice UUID como claves primarias. No cifre los ID para ocultarlos: hacerlo de forma segura es difícil.

Qué debe entrar en la revisión de código

Lea todos los handlers que reciben un ID proporcionado por el cliente y localice dónde se realiza la comprobación de permisos. Fíjese primero en si la consulta se ejecuta sobre un conjunto acotado al usuario o llama a un find global y comprueba después, si es que lo hace. A continuación confirme que la comprobación existe en todas las funciones que llegan a un registro a partir de datos del cliente, incluidas la actualización, el borrado y la exportación.

Desconfíe de un handler que se limite a comparar el ID del usuario de la sesión (por ejemplo extraído del JWT, que puede inspeccionar con nuestra herramienta de JWT) con el parámetro ID. OWASP señala que esto solo cubre un pequeño subconjunto de casos. La autorización debe seguir las políticas y la jerarquía de los usuarios. En un sistema multi-tenant, un administrador del tenant X puede solicitar una factura que pertenece al tenant Y. El usuario está autenticado, el rol es de administrador y el ID es válido, de modo que una simple comprobación de rol puede darse por buena mientras el objeto está en el tenant equivocado.

Al hacer el triaje, distinga dos fallos. En BOLA el usuario tiene permiso, por diseño, para llamar al endpoint, y la infracción es el ID manipulado. Si un usuario llega a un endpoint al que no debería llegar en absoluto, se trata de autorización rota a nivel de función, cuya corrección es distinta.

Por dónde empezar

  1. Inventaríe todos los endpoints, formularios y operaciones GraphQL que aceptan un identificador, incluidos los nombres de fichero y los campos ocultos. Parta de su especificación OpenAPI o de su tabla de rutas y añada después lo que solo aparece en el historial del proxy al navegar por la aplicación.
  2. Ejecute la pasada con dos usuarios descrita arriba sobre los diez objetos más sensibles, como facturas, datos personales y acciones de administración, y registre cada respuesta que no sea una denegación.
  3. Sustituya las búsquedas globales por búsquedas acotadas al usuario, utilizando el mecanismo de autorización de su framework.
  4. Incorpore a su pipeline las pruebas parametrizadas entre usuarios y haga que un fallo bloquee la publicación.
  5. Añada las preguntas de revisión a su plantilla de pull request. Para una comprobación externa de lo que sus pruebas hayan pasado por alto, puede contratar nuestro servicio de pruebas de penetración.

Cómo podemos ayudarle

Pruebas de penetración

Atacamos sus redes, sus aplicaciones y a su personal como lo haría un adversario real, y le mostramos cómo cerrar cada brecha.

Decodificador JWT

Decodifique un JSON Web Token y busque errores de seguridad habituales, sin que salga de su navegador.

Contacte con nosotros
Equipo de AngelForce

Escrito por el equipo de AngelForce: consultores de seguridad con certificaciones CEH, LPT Master y CISA que prueban, supervisan y protegen los sistemas de organizaciones de toda Europa y Türkiye.

Redactado con ayuda de IA a partir de las fuentes públicas que se indican más abajo y revisado por el equipo de AngelForce antes de su publicación.

Fuentes

  1. cheatsheetseries.owasp.org/cheatsheets/Insecure_Direct_Object_Reference_Prevention_Cheat_Sheet.html
  2. api-security.owasp.org/editions/2023/en/0xa1-broken-object-level-authorization/

Artículos sobre Seguridad web

Cuéntenos qué necesita proteger

Describa su entorno y lo que quiere conseguir. Le responderemos por correo electrónico.

Contacte con nosotros
[email protected]

AngelForce OÜ · Tallinn, Estonia