CyberKareem/kameFHIR

GitHub: CyberKareem/kameFHIR

一个故意存在跨租户越权/IDOR漏洞的FHIR风格医疗API靶场,用于安全培训中复现和修复授权缺陷。

Stars: 0 | Forks: 0

# kameFHIR 一个微型的 **FHIR 风格**医疗保健 API (Node/Express),作为一个实操实验室而构建, 用于复现由 **CVE-2026-59979** 通用描述的跨租户越权 / IDOR 漏洞类型。 它通过内存存储公开了 FHIR 风格的资源 —— `Organization` (租户)、`Patient`、`HealthcareService` 和 `Task` —— 因此只需一条命令即可运行整个项目,无需数据库。 有趣的部分在于:**同一资源的同级路由具有不同的 授权机制。** 一些会更改状态的路由正确地执行了租户 权限检查;而它们的同级路由仅进行了身份验证。切换一个环境 变量 (`SECURE_MODE`) 即可查看修复后的行为。 ## 快速开始 ### 选项 A — Docker (单条命令,相互隔离) ``` docker compose up --build ``` 该服务**仅**发布在 **`127.0.0.1:3000`** (环回地址) 上 —— 它不会 暴露给你的网络。打开 即可访问落地页。 ### 选项 B — 原生 Node ``` npm install npm start # fixed 模式:npm run start:secure ``` 需要 Node ≥ 18 (使用内置的测试运行器和 `fetch`)。默认绑定到 `127.0.0.1:3000`。 ### 运行测试 (离线) ``` npm test ``` 测试套件会在临时的 localhost 端口上启动应用,并断言在默认模式下跨租户的 `DELETE /Task/:id` 会**成功**,而在 `SECURE_MODE=true` 时会被**拦截** (`403`)。不会向外部主机发起任何网络调用。 ## 不可忽视的警告 (置于四处) 由于仓库名称本身并不代表危险,因此“仅用于培训”的警告 出现在四个位置: 1. 本 README 顶部的粗体横幅。 2. 落地页 (`GET /`) 上醒目的红色通知。 3. `docker-compose.yml` 的头部注释块。 4. 服务器启动时打印的启动日志行。 ## 内置身份与 Bearer Token 包含两个租户,四个具有不同权限级别的用户。Token 在启动时使用 `JWT_SECRET` 进行签名,并**打印在启动日志中**,显示在落地页 (`GET /`) 上,以及通过 **`GET /_dev/tokens`** 以 JSON 格式提供。(由于 Token 的值取决于 `JWT_SECRET`,请从这些来源之一复制实时的 Token, 而不是将其硬编码。) | 名称 | 用户 ID | 组织 (租户) | 角色 | 获取 Token 的位置 | | --- | --- | --- | --- | --- | | Ada Admin | `user-a-admin` | `org-a` (Alpha Health Network) | admin | 启动日志 / `GET /_dev/tokens` / `GET /` | | Nora Nurse | `user-a-nurse` | `org-a` (Alpha Health Network) | nurse | 启动日志 / `GET /_dev/tokens` / `GET /` | | Ben Admin | `user-b-admin` | `org-b` (Beta Medical Group) | admin | 启动日志 / `GET /_dev/tokens` / `GET /` | | Nick Nurse | `user-b-nurse` | `org-b` (Beta Medical Group) | nurse | 启动日志 / `GET /_dev/tokens` / `GET /` | 快速获取一个: ``` A_NURSE=$(curl -s http://127.0.0.1:3000/_dev/tokens \ | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{console.log(JSON.parse(s).tokens.find(x=>x.id==="user-a-nurse").token)})') ``` 将其作为 `Authorization: Bearer ` 发送。 ## API 接口 | 方法与路由 | 守卫机制 (默认模式) | 说明 | | --- | --- | --- | | `GET /` | 无 | 落地页 + 安全通知 + Token 表格 | | `GET /_dev/tokens` | 无 | 开发辅助:内置的 Bearer Token | | `GET /health` | 无 | 存活检测 | | `GET /Organization` | `requireAuth` | 列出**所有**租户 (枚举) | | `GET /Organization/:id` | 仅 `requireAuth` | 🔴 跨租户读取 | | `GET /Patient` | `requireAuth` | 限定在你自己的组织内 (安全) | | `GET /Patient/:id/summary` | **无** | 🔴 未经身份验证的 PII 泄露 | | `GET /Patient/:id` | 仅 `requireAuth` | 🔴 跨租户读取 | | `POST /Patient` | `requireAuth` | 在你自己的组织内创建 (安全) | | `PUT /Patient/:id` | `requireAuth` + `requireOrgPermission` | 🟢 安全的同级路由 | | `GET /HealthcareService` | `requireAuth` | 限定范围 (安全) | | `GET /HealthcareService/:id` | 仅 `requireAuth` | 🔴 跨租户读取 | | `POST /HealthcareService` | `requireAuth` | 在你自己的组织内创建 (安全) | | `PUT /HealthcareService/:id` | `requireAuth` + `requireOrgPermission` | 🟢 安全的同级路由 | | `GET /Task` | `requireAuth` | 限定范围 (安全) | | `GET /Task/:id` | 仅 `requireAuth` | 🔴 跨租户读取 | | `POST /Task` | `requireAuth` | 在你自己的组织内创建 (安全) | | `PUT /Task/:id` | `requireAuth` + `requireOrgPermission` | 🟢 安全的同级路由 | | `DELETE /Task/:id` | 仅 `requireAuth` | 🔴 跨租户删除 (核心漏洞) | 🔴 = 在默认模式下故意设计为易受攻击的。 🟢 = 在两种模式下均正确授权。 当 `SECURE_MODE=true` 时,每一个 🔴 行都会升级为强制执行租户 权限检查 (并且未经身份验证的 `/summary` 开始要求身份验证)。 ## 学习目标 - 区分 **身份验证 (authentication)** 与 **授权 (authorization)**:经过验证的 Token (`requireAuth`) 并不意味着调用者有权操作*此*资源。 - 识别 **同级路由不对称性** —— `PUT /Task/:id` 执行了租户检查, 但 `DELETE /Task/:id` / `GET /Task/:id` 却没有。 - 认识到数据访问中 **缺失的租户过滤**:`findById(id)` 与 `findByIdScoped(id, orgId)`。 - 识别出 **完全忘记身份验证** 的路由 (`GET /Patient/:id/summary`)。 - 练习修复方法:在**每一个**会更改状态的路由上强制执行租户/组织检查,并将每次查询范围限定为调用者的租户。 基于 curl 的完整漏洞利用演示和针对每个漏洞的修复方法详见 [`docs/VULNERABILITIES.md`](docs/VULNERABILITIES.md)。 ## 切换 `SECURE_MODE` 查看修复效果 `SECURE_MODE` 从环境中读取,默认值为 `false`。 ``` # vulnerable(默认) npm start # fixed SECURE_MODE=true npm start # or: npm run start:secure # docker:在 docker-compose.yml 中设置 SECURE_MODE: "true",然后执行 `docker compose up` ``` 当 `SECURE_MODE=true` 时,应用程序会: 1. 将 `requireOrgPermission` 应用于**所有**更改状态的 (以及之前仅限身份验证的) 路由,并且 2. 将数据访问从 `findById(id)` 切换为 `findByIdScoped(id, orgId)`。 随后,跨租户的读取/写入/删除操作将返回 `403`,而未经身份验证的 `/summary` 接口将返回 `401`。通过切换这一个变量即可对比漏洞行为与修复后的行为 —— 无需单独的分支。 ## 项目结构 ``` kameFHIR/ ├── server.js # entry point (binds 127.0.0.1; prints boot warning) ├── src/ │ ├── app.js # createApp() factory + landing page │ ├── auth.js # requireAuth (authentication ONLY) + issueToken │ ├── authz.js # requireOrgPermission (the tenant check) │ ├── store.js # in-memory store: findById vs findByIdScoped │ ├── seed.js # 2 tenants, 4 users, seed resources │ ├── fhir.js # small FHIR JSON helpers │ └── routes/ │ ├── organizations.js │ ├── patients.js │ ├── services.js │ └── tasks.js ├── test/ │ └── crosstenant.test.js # offline node:test proving vuln vs fixed ├── docs/ │ └── VULNERABILITIES.md # per-flaw exploit + fix walkthrough ├── docker-compose.yml # one-command isolated run (127.0.0.1 only) ├── Dockerfile ├── .env.example ├── package.json ├── LICENSE └── README.md ``` ## 配置 | 变量 | 默认值 | 含义 | | --- | --- | --- | | `PORT` | `3000` | 监听端口 | | `HOST` | `127.0.0.1` | 绑定的网卡接口 (Docker 内部设置为 `0.0.0.0`;宿主机端口保持为环回地址) | | `JWT_SECRET` | `kamefhir-insecure-training-secret` | 用于签名/验证演示用的 Bearer JWT | | `SECURE_MODE` | `false` | 设为 `true` 则应用修复 (执行租户检查 + 限定查询范围) | 参见 [`.env.example`](.env.example)。 ## 参考 总体参照 **CVE-2026-59979** (跨租户越权 / IDOR) 进行建模。**CVE-2026-59889** (不安全的多态反序列化) 属于不同且不相关的类别,此处仅作对比之用。未在任何地方使用真实的公司、 产品或目标名称 —— 所有身份和数据均为虚构。 ## 许可证 MIT © 2026 Abdullah Kareem (CyberKareem) — 。参见 [`LICENSE`](LICENSE)。
标签:GNU通用公共许可证, MITM代理, Node.js, OPA, Web API, 医疗信息系统, 版权保护, 自定义脚本, 请求拦截, 越权漏洞, 身份认证与访问控制, 靶场