
HDU IS Work 1 - API REST sicura (Flask) con CI/CD (pytest + bandit + pip-audit) e analisi di CVE-2018-17246
API REST sicura in Python/Flask, CI/CD tramite GitHub Actions (SAST + SCA + test automatizzati), e un'analisi completa di una CVE reale riprodotta in un ambiente Docker (Kibana LFI, CVE-2018-17246).
Nome: Guhaotian Numero HDU: 23320209
https://github.com/ght657/work1-secure-api (pubblico)POST /auth/login — rilascia un JWT firmato dopo aver verificato l'hash della password.GET /api/data — protetto da JWT (Bearer token); restituisce i record dell'utente corrente.GET /api/users/search?q= — endpoint di lettura protetto con query SQL a parametri vincolati.POST /api/posts (protetto da JWT) che dimostra la protezione contro XSS memorizzato / escaping dell'output.File del workflow: .github/workflows/ci.yml, attivato a ogni push su main.
| Job | Strumenti | Cosa fa | Evidenza |
|---|---|---|---|
test | pytest 8 | esegue la suite di 11 test | evidence/07_pytest_run.png |
sast | Bandit 1.8.0 | analisi statica di app/, tests/ (report JSON caricato come artifact) | evidence/08_bandit_report.png |
sca | pip-audit 2.8.0 | audit delle dipendenze (report JSON caricato come artifact) | evidence/09_pip_audit_report.png |
La pipeline fallisce se un test fallisce o se rimane qualsiasi risultato SAST/SCA, quindi una pipeline rossa significa che è necessario effettuare il push di una correzione prima del merge.
11 test, tutti superati (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
Il codice dell'applicazione è nostro, quindi una prima esecuzione non contiene naturalmente rumore di terze parti. Risultato: 0 problemi:
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 elenca tutte le dipendenze con CVE note; l'esecuzione iniziale ha rilevato:
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 voci in totale (Flask, PyJWT ×17, python-dotenv). Ho aggiornato alle versioni corrette:
Flask 3.1.3, PyJWT 2.15.0 e rimosso python-dotenv (solo helper di configurazione opzionale —
la versione corretta 1.2.2 richiede Python ≥ 3.10 mentre il venv di audit è 3.9).
Dopo la correzione:
No known vulnerabilities found
(evidence/08b_bandit_after_fix.png, evidence/09b_pip_audit_after_fix.png)
L'API è stata testata contro gli attacchi classici con curl. Gli screenshot sono in evidence/:
POST /auth/login con username: "admin'--" restituisce 401 Invalid credentials —
la ricerca utilizza una query ORM parametrizzata sulle credenziali hashate, il payload
iniettato rimane un dato e non diventa mai SQL.
GET /api/users/search?q=%' OR '1'='1 → la stringa viene associata come parametro a
SELECT ... WHERE username LIKE ? (caratteri jolly sottoposti a escape), quindi non corrisponde a nulla e l'API
restituisce una lista vuota — nessun errore e nessun dump dell'intera tabella.
POST /api/posts con title: "<script>alert('xss')</script>"; il server memorizza la
stringa grezza ma la sottopone a escape in output (markupsafe.escape + jsonify), quindi il payload
restituito è <script>alert('xss')</script> — un browser visualizza il testo, non lo
esegue. (La riproduzione con il rendering nel browser è mostrata in evidence/06_xss_readback.png.)
401 {"error": "Missing or malformed Authorization header"} (evidence/03_data_denied.png)200 + dati protetti (evidence/04_data_with_token.png)401 (verifica della firma con HS256)401 (scadenza di 60 minuti)Authorization: Bearer <token>
(evidence/10_jwt_structure.png mostra la struttura in 3 parti).Le password sono memorizzate come hash bcrypt (12 round) e non vengono mai restituite da alcun endpoint
(test test_passwords_stored_as_bcrypt_hashes).
/etc/passwd) e — poiché la route vulnerabile richiede il file come
modulo JS — se è raggiungibile un percorso scrivibile (ad es. tramite un'altra falla o un
file temporaneo generato), il JS controllato dall'attaccante può essere eseguito dal processo Kibana.GET /api/console/api_server?apis=../../../../../../etc/passwd — il parametro apis
viene passato a require() dopo un solo semplice controllo del percorso, consentendo a sequenze ..
di uscire dalla directory api lato server e leggere/eseguire file arbitrari.Utilizzo l'immagine pubblica vulhub (già pubblicata, verificata come vulnerabile):
vulhub/kibana:5.6.12 + vulhub/elasticsearch:5.6.16 — vedi
CVE-2018-17246/docker-compose.yml (cartella con lo stesso nome in questo repo).
cd CVE-2018-17246
docker compose up -d # kibana 5.6.12 + elasticsearch 5.6.16, UI on :5601
Passo 1 — attivare l'LFI (l'API Console accetta un percorso apis che viene risolto come file):
curl -i "http://localhost:5601/api/console/api_server?sense_version=%40%40SENSE_VERSION&apis=../../../../../../../../../../../etc/passwd"
La risposta è 500 (il file non è un modulo JS valido) ma il contenuto del file appare nel
log del server — Kibana legge il file e tenta di eseguire require() su di esso:
docker compose logs kibana | grep -i "passwd\|root:x"
Passo 2 — escalatione all'esecuzione di codice. Un file JS locale viene caricato come modulo dalla
stessa route: /tmp/vulhub.js contenente una piccola "webshell" che esegue un comando di shell quando
la sua funzione esportata viene invocata dall'API Console: