
Encuentra rutas de API con autorización más débil que sus homólogas. Recuperó CVE-2026-45316 del código fuente. Incluye los resultados negativos.
Encuentra rutas de API que llevan un control de autorización más débil que el de sus rutas hermanas.
Encontró CVE-2026-45316 en Open WebUI solo a partir del código fuente, sin conocimiento de avisos de seguridad:
[MISMATCH] POST /notes/{id}/pin (notes.py:pin_note_by_id)
3/3 comparable write operations on note require has_access(write);
this route requires only has_access(read)
POST /notes/{id}/update has_access(write)
POST /notes/{id}/access/update has_access(write)
DELETE /notes/{id}/delete has_access(write)
En 2.568 rutas en cinco bases de código en producción produjo 4 hallazgos. Dos eran reales. Este README explica ambos números.
Una gran clase de errores de autorización no es una comprobación rota. Es una comprobación faltante o debilitada, en una ruta cuyas hermanas todas la hicieron bien.
Portainer autorizó cuatro endpoints de plantillas hermanos y no al quinto. Signal K limitó la tasa de inicio de sesión HTTP pero no la de WebSocket. La ruta de pin de notas de Open WebUI mutaba una nota mientras comprobaba el permiso de lectura, cuando cualquier otra mutación de notas comprobaba escritura.
Siempre la misma forma:
✓ ✓ ✓ ✓ ✗
El repositorio ya contiene la regla. Una ruta la rompió. Así que reconstruya la regla desde el código e informe la excepción. Sin archivo de política, sin configuración, sin anotaciones. El conjunto de prueba son las propias rutas hermanas.
python3 enforcement_coverage.py /path/to/repo
python3 enforcement_coverage.py /path/to/repo --density
python3 enforcement_coverage.py /path/to/repo --json
Python 3.10+, sin dependencias. Solo FastAPI.
Los veredictos son MISSING, MISMATCH, PRESERVED, UNKNOWN. UNKNOWN se abstiene y nunca se informa.
Extracción. Resuelve controles a partir de la firma Depends(), el decorador dependencies=[], dependencias a nivel de enrutador, cuerpos de función, funciones envoltorio y listas de clases de permisos.
La extracción del cuerpo es esencial. En Open WebUI, 216 de 608 manejadores llevan la comprobación decisiva dentro de la función:
if user.role != 'admin' and not await AccessGrants.has_access(
user_id=user.id, resource_type='note',
resource_id=note.id, permission='write', db=db,
):
raise HTTPException(status_code=403)
La clase de operación proviene de lo que el manejador hace con el recurso, no del verbo HTTP. POST /notes/{id}/chat es un POST que lee una nota.
Clasificación de vocabulario. Dos valores en una misma familia de control no forman necesariamente una escala de fuerza:
has_access {read, write} LEVEL
ensure_flow_permission {create,delete,execute,read,write} ACTION
has_permission {features.notes, workspace.tools} SCOPE
Solo los vocabularios LEVEL admiten comparación de fuerza. Comparar FlowAction.CREATE con un WRITE dominante produjo seis falsos positivos en Langflow antes de que esto se corrigiera.
Filtro de dirección. Solo se informan desviaciones hacia un control menos restrictivo. Ser más estricto que el precedente no es una vulnerabilidad.
Dos verdaderos positivos.
POST /notes/{id}/pin en Open WebUI — CVE-2026-45316.
Un hallazgo adicional es una asimetría de permisos en Netflix Dispatch:
POST /{incident_id}/resources usa IncidentViewPermission mientras que seis operaciones de escritura hermanas usan IncidentEditPermission.
IncidentViewPermission devuelve True para cualquier incidente no restringido;
IncidentEditPermission requiere administrador, comandante o reportero. El manejador pone en cola la creación de tickets y grupos sin más comprobaciones.
No se informó porque no hay lugar donde informarlo: el repositorio fue archivado por Netflix el 3 de septiembre de 2025 y es de solo lectura, no tiene SECURITY.md, el informe privado de vulnerabilidades está deshabilitado para repositorios archivados, y Dispatch está explícitamente fuera de alcance en el programa de recompensas de Netflix. Publicarlo aquí es el único canal de divulgación restante. Es de gravedad baja — requiere un miembro autenticado de la organización y afecta solo a incidentes no restringidos — y el proyecto ya no se mantiene.
Dos falsos positivos. POST /tools/{id}/valves/user/update escribe la configuración de válvulas propia del usuario y legítimamente solo necesita lectura sobre la herramienta — el objetivo de la mutación es una entidad diferente al sujeto de autorización. Y una ruta de base de conocimiento de Langflow donde el control de la ruta hermana no es necesario.
Esta es la parte útil.
Danswer tiene un 95,9 % de cobertura de control y produjo cero hallazgos con un 80,7 % de UNKNOWN. Su vocabulario es require_permission('basic_access'), ('manage_connectors') — un espacio de nombres de capacidades, no una escala de fuerza. No se puede decir que manage_connectors sea más débil que read_connectors.
Lo que predice hallazgos es una llamada de permiso con ámbito de recurso y un argumento de fuerza ordenado, como has_access(resource, read|write). Una de cada cinco bases de código tenía eso.
Cinco patrones en cinco repositorios. Cada uno requirió trabajo de extracción antes de que el análisis pudiera ejecutarse en absoluto.
En cualquier repositorio, bajo cualquier configuración. La mayoría de las rutas no pertenecen a una familia de rutas hermanas de tres o más con un control consistente.
Ninguno de estos fue anticipado de antemano.
Depends()El defecto 13 es el interesante. Las rutas de recomendación de etiquetas de Dispatch carecen del CaseViewPermission que llevan sus hermanas. Pero ese permiso devuelve True para cualquier caso no restringido, y el servicio ya comprueba visibility == restricted en línea. Los caminos son equivalentes. Detectar eso requiere análisis de equivalencia semántica, no estructura.
El mecanismo funciona. Recuperó un CVE publicado y una brecha de autorización no reportada a partir del código fuente, con conjuntos de prueba auditables, usando solo información disponible antes de que el commit vulnerable se fusionara.
El rendimiento es de aproximadamente un hallazgo por cada 1.000 rutas, y necesita una arquitectura que una de cinco bases de código sustanciales tenía.
Útil como herramienta de auditoría para una base de código con un vocabulario de permisos con ámbito de recurso. No es, según esta evidencia, un escáner de propósito general.
La detección estática de vulnerabilidades de control de acceso mediante la inferencia de supuestos implícitos se remonta a USENIX Security 2011. ACMiner extrajo comprobaciones de autorización en el middleware de Android. Semgrep incluye detección impulsada por IA orientada a la autorización faltante y reportó una precisión del 61 % en una evaluación de un cliente. OWASP publica una hoja de referencia de Pruebas de Regresión de Autorización cuyo enfoque recomendado es mantener una matriz Actor × Recurso × Acción a mano.
Este es un enfoque limitado y determinista: recuperar solo lo que prueban las rutas hermanas, abstenerse en caso contrario.
Licencia MIT.
| repositorio | rutas | controladas | hallazgos |
|---|
| LiteLLM | 808 | 87.1% | 0 |
| Danswer / Onyx | 653 | 95.9% | 0 |
| Open WebUI | 529 | 97.2% | 2 |
| Netflix Dispatch | 291 | 36.1% | 1 |
| Langflow | 287 | 47.4% | 1 |
| repositorio | patrón |
|---|
| Open WebUI | AccessGrants.has_access(resource_type=, permission=) |
| Langflow | ensure_<resource>_permission(user, Action.X) |
| LiteLLM | comparación de rol sobre user_api_key_dict.user_role |
| Netflix Dispatch | Depends(PermissionsDependency([CaseEditPermission])) |
| Danswer | require_permission('basic_access') |