saltar al contenido
volver al mapa
técnica
etapa: design
3 min de lectura

A01 Broken Access Control

La autenticación dice quién eres; el control de acceso dice qué puedes hacer. Cuando el servidor olvida verificar en cada solicitud, cambiar un id en una URL es todo lo que necesita un atacante.

Burp Suite (Autorize)OWASP ZAPPostman or curl with two accountsSemgrepCasbin / OPA

Qué es

El control de acceso aplica la política para que los usuarios no actúen fuera de sus permisos previstos. Se rompe cuando el servidor confía en algo que controla el cliente: un id en la URL, un campo oculto, un claim de rol, una cookie. El resultado es leer datos de otras personas, modificarlos o llegar a funciones de administración.

El :2025 lo lista como A01. Las dos formas cotidianas son (insecure direct object reference, también llamado en seguridad de APIs): la solicitud nombra un objeto y el servidor lo devuelve sin preguntar si quien llama es su dueño; y la falta de verificaciones a nivel de función: existe un endpoint para administradores, pero nada impide que un usuario normal lo llame.

Cómo funciona el ataque

El atacante inicia sesión como usuario normal, abre la pestaña de red del navegador y encuentra una solicitud como GET /api/invoices/1041. Cambia 1041 por 1042 y lee la factura de otra persona. Si los ids son secuenciales, un script de diez líneas recorre toda la tabla.

  • Cambia un id en el path, en el query string, en el cuerpo JSON o en un header, y observa si la respuesta cambia de dueño.
  • Llama directamente a una ruta de admin o interna (/admin/users, DELETE /api/users/7) con una sesión normal.
  • Envía campos adicionales que el formulario nunca muestra ("role": "admin"): el deja que el cliente defina lo que no debería.
  • Ocultar el botón en la UI: la UI no es un control, el servidor sí.

Código vulnerable

Cada handler verifica que el usuario haya iniciado sesión y luego carga el objeto solo por id. Nada vincula la factura con quien llama.

invoices.js (Express + Prisma)javascript
app.get("/api/invoices/:id", requireLogin, async (req, res) => {
  const invoice = await db.invoice.findUnique({ where: { id: Number(req.params.id) } });
  res.json(invoice); // cualquier usuario autenticado puede leer cualquier factura
});
invoices.py (Flask + SQLAlchemy)python
@app.get("/api/invoices/<int:invoice_id>")
@login_required
def get_invoice(invoice_id):
    invoice = Invoice.query.get_or_404(invoice_id)
    return jsonify(invoice.to_dict())
InvoiceController.java (Spring)java
@GetMapping("/api/invoices/{id}")
public Invoice get(@PathVariable long id) {
    return repo.findById(id).orElseThrow(NotFoundException::new);
}

La corrección

Pon la propiedad en la propia consulta, de modo que una fila que no es tuya simplemente no se encuentre. Responde igual para "no existe" y "no es tuya", para que la respuesta no confirme qué ids son válidos. La regla es la misma en cualquier lenguaje: autoriza en el servidor, en cada solicitud, denegando por defecto.

invoices.jsjavascript
app.get("/api/invoices/:id", requireLogin, async (req, res) => {
  const invoice = await db.invoice.findFirst({
    where: { id: Number(req.params.id), ownerId: req.user.id },
  });
  if (!invoice) return res.status(404).end();
  res.json(invoice);
});
invoices.pypython
@app.get("/api/invoices/<int:invoice_id>")
@login_required
def get_invoice(invoice_id):
    invoice = Invoice.query.filter_by(
        id=invoice_id, owner_id=current_user.id
    ).first_or_404()
    return jsonify(invoice.to_dict())
InvoiceController.javajava
@GetMapping("/api/invoices/{id}")
public Invoice get(@PathVariable long id, @AuthenticationPrincipal User user) {
    return repo.findByIdAndOwnerId(id, user.getId())
               .orElseThrow(NotFoundException::new);
}
admin.js (verificación a nivel de función, denegar por defecto)javascript
const requireRole = (role) => (req, res, next) =>
  req.user?.roles?.includes(role) ? next() : res.status(403).end();

app.delete("/api/users/:id", requireLogin, requireRole("admin"), deleteUser);

Demuestra la corrección

Una prueba de regresión usa dos usuarios. El usuario A crea una factura; el usuario B la solicita y debe recibir 404 (o 403), y una llamada anónima debe recibir 401. Ejecútala en el CI para que un refactor no elimine la verificación sin que nadie lo note. La misma forma de prueba funciona en cualquier lenguaje y para cualquier tipo de objeto.

test_invoices_access.py (pytest)python
def test_user_cannot_read_other_users_invoice(client, alice, bob):
    invoice_id = client.post("/api/invoices", json={"total": 10}, auth=alice).json()["id"]

    assert client.get(f"/api/invoices/{invoice_id}", auth=alice).status_code == 200
    assert client.get(f"/api/invoices/{invoice_id}", auth=bob).status_code == 404
    assert client.get(f"/api/invoices/{invoice_id}").status_code == 401
invoices.test.js (supertest)javascript
test("bob cannot read alice's invoice", async () => {
  const { body } = await request(app).post("/api/invoices").set(alice).send({ total: 10 });
  await request(app).get(`/api/invoices/${body.id}`).set(bob).expect(404);
  await request(app).get(`/api/invoices/${body.id}`).expect(401);
});

Checklist

  • : una ruta sin regla explícita está prohibida, no abierta.
  • Aplica la propiedad en un solo lugar compartido (un helper de consultas, un policy engine), no copiando verificaciones en cada handler.
  • No confíes en ids, roles ni precios que vienen del cliente. Deriva la identidad de la sesión.
  • Usa ids impredecibles cuando sirva (UUIDs), pero nunca como único control: un id imposible de adivinar no es autorización.
  • Registra los fallos de control de acceso y alerta ante ráfagas: alguien enumerando ids es visible.
  • Prueba con dos cuentas en cada release. Los scanners automáticos rara vez entienden quién debería ver qué.
OWASP Top 10La lista de referencia más conocida de riesgos de seguridad en aplicaciones web (A01 control de acceso roto, A02 fallas criptográficas, A03 inyección...), revisada cada pocos años por la comunidad OWASP. Es un mapa educativo de categorías de riesgo, no un checklist de herramientas ni una certificación.ver en el glosario →IDORreferencia directa insegura a objetos · BOLALa solicitud nombra un objeto (un id) y el servidor lo devuelve sin comprobar que quien llama puede verlo. Es la forma más común de control de acceso roto y, en las APIs, se llama broken object level authorization.ver en el glosario →BOLAbroken object level authorizationUna API acepta el id de un objeto y lo devuelve sin verificar si quien llama puede acceder a él. Es la forma de IDOR en las APIs.ver en el glosario →Mass assignmentUna API vincula campos enviados por el cliente a un objeto, permitiendo que quien llama defina campos (como un rol) que no debería controlar.ver en el glosario →Denegar por defectoEl acceso está prohibido a menos que una regla lo permita explícitamente, de modo que una ruta olvidada falla cerrada en lugar de abierta.ver en el glosario →

conexiones en el mapa

actualizado el 14/10/2026 · fuentes para profundizar · reportar un error

en otros dominios

Desbloquea

pon a prueba lo que entendiste

Cargando preguntas…