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, 医疗信息系统, 版权保护, 自定义脚本, 请求拦截, 越权漏洞, 身份认证与访问控制, 靶场