A10 Mishandling of Exceptional Conditions
La seguridad a menudo depende de lo que pasa cuando algo sale mal. Si un error significa "permitir", un stack trace significa "aquí está mi código" o una operación a medias significa "estado corrupto", el camino de fallo es la vulnerabilidad.
Qué es
El manejo inadecuado de condiciones excepcionales es la preparación, detección y respuesta indebidas ante situaciones inusuales: dependencias que fallan, timeouts, entradas mal formadas, agotamiento de recursos, escrituras parciales y estados inesperados. Los errores típicos son fallar abierto, filtrar detalles en los mensajes de error, tragarse las excepciones y dejar transacciones a medias.
Es una categoría nueva en el :2025 (A10) y reúne varias debilidades más antiguas en torno al manejo de errores.
Cómo funciona el ataque
- Fail open: un servicio de autorización agota el tiempo de espera y el código trata la excepción como "sin restricción".
- Fuga de información: un error de base de datos se devuelve al cliente con el nombre de la tabla, la consulta y la versión del driver.
- Errores tragados: un catch vacío oculta una verificación de firma fallida y el procesamiento continúa.
- Estado parcial: se cobra un pago pero falla la escritura del pedido; reintentar duplica el cobro.
- Agotamiento de recursos: una entrada sin límite o una excepción no controlada en un bucle permite que una sola solicitud tumbe el servicio.
Código vulnerable
La verificación falla abierta, el error filtra detalles internos y la actualización de varios pasos no es atómica.
def can_access(user, doc):
try:
return policy_service.allowed(user.id, doc.id)
except Exception:
return True # el servicio está caído, así que déjalos pasar
@app.errorhandler(Exception)
def on_error(e):
return str(e), 500 # filtra detalles internos al clienteawait accounts.debit(from, amount);
await accounts.credit(to, amount); // si esto lanza un error, el dinero desaparecióLa corrección
Falla cerrado: ante cualquier duda sobre una decisión de seguridad, deniega. Devuelve al cliente un mensaje genérico con un y deja los detalles en los logs. Haz atómicos los cambios de varios pasos con una transacción y acota todo: tiempo, tamaño, reintentos, memoria.
def can_access(user, doc):
try:
return policy_service.allowed(user.id, doc.id, timeout=0.5)
except Exception:
log.exception("policy check failed", extra={"user": user.id, "doc": doc.id})
return False # falla cerrado
@app.errorhandler(Exception)
def on_error(e):
rid = uuid4().hex
log.exception("unhandled error", extra={"request_id": rid})
return {"error": "internal error", "request_id": rid}, 500await db.transaction(async (trx) => {
await accounts.debit(from, amount, trx);
await accounts.credit(to, amount, trx); // ocurren ambos o ninguno
});ctx, cancel := context.WithTimeout(r.Context(), 800*time.Millisecond)
defer cancel()
resp, err := policy.Check(ctx, user, doc)
if err != nil {
log.Error("policy check failed", "err", err)
http.Error(w, "forbidden", http.StatusForbidden) // falla cerrado
return
}Demuestra la corrección
Inyecta el fallo a propósito. Haz que la dependencia agote el tiempo de espera y verifica que se deniega el acceso; haz que el segundo paso lance un error y verifica que nada cambió; provoca un error y verifica que la respuesta no incluye detalles internos. El fuzzing agrega entradas extrañas en las que no pensaste.
def test_denies_when_policy_service_is_down(monkeypatch, user, doc):
monkeypatch.setattr(policy_service, "allowed", lambda *a, **k: (_ for _ in ()).throw(TimeoutError()))
assert can_access(user, doc) is False
def test_error_response_has_no_internals(client, monkeypatch):
monkeypatch.setattr(db, "query", lambda *a, **k: (_ for _ in ()).throw(RuntimeError("relation users_secret does not exist")))
body = client.get("/api/things").get_json()
assert "users_secret" not in str(body) and "request_id" in body
@given(st.text())
def test_parser_never_crashes_on_any_text(s): # Hypothesis
parse_input(s) # puede rechazar (ValueError) pero no debe lanzar ninguna otra excepcióntest("a failed credit rolls back the debit", async () => {
jest.spyOn(accounts, "credit").mockRejectedValue(new Error("boom"));
const before = await accounts.balance(from);
await expect(transfer(from, to, 50)).rejects.toThrow();
expect(await accounts.balance(from)).toBe(before);
});Checklist
- Decide de antemano el comportamiento de fallo de cada verificación de seguridad: debe denegar.
- Captura de forma acotada; nunca dejes un catch vacío; registra con contexto y vuelve a lanzar la excepción o devuelve un error seguro.
- Devuelve errores genéricos con un correlation id; deja los stack traces y las consultas solo en los logs.
- Usa transacciones y claves de idempotencia en las operaciones de varios pasos, para que los reintentos sean seguros.
- Define timeouts, límites de tamaño, rate limits y presupuestos de reintentos en cada llamada externa y entrada.
- Practica el fallo: la inyección de fallos, el fuzzing y los game days revelan los caminos fail-open antes que los atacantes.
conexiones en el mapa
- A09 Security Logging and Alerting FailuresregistraA10 Mishandling of Exceptional Conditions
actualizado el 14/10/2026 · fuentes para profundizar · reportar un error
en otros dominios
- Confiabilidad a escala: idempotencia, reintentos y backpressuresecengfallar cerrado y de forma idempotente
Estudia antes
pon a prueba lo que entendiste
Cargando preguntas…