halilibrahimd27/tenant-trace

GitHub: halilibrahimd27/tenant-trace

多租户隔离审计工具,通过 canary 数据注入和动态探测机制,精确验证并报告租户间的跨租户数据泄露,可作为 CI 合并门限运行。

Stars: 0 | Forks: 0

[![verify](https://static.pigsec.cn/wp-content/uploads/repos/cas/f5/f5acfe8dce6a25e85907ab3b9795e0d0a81b502cf4ae76bb4079cea10546c57e.svg)](https://github.com/halilibrahimd27/tenant-trace/actions/workflows/verify.yml) [![release](https://img.shields.io/github/v/release/halilibrahimd27/tenant-trace?color=blue)](https://github.com/halilibrahimd27/tenant-trace/releases) [![ghcr](https://img.shields.io/badge/ghcr.io-tenant--trace-blue?logo=docker&logoColor=white)](https://github.com/halilibrahimd27/tenant-trace/pkgs/container/tenant-trace) [![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![python 3.12+](https://img.shields.io/badge/python-3.12%2B-blue.svg)](pyproject.toml) # TenantTrace **证明租户 A 是否能访问租户 B 的数据。** 一个面向 SaaS 应用的多租户隔离审计工具:它能发现租户之间失效的对象级授权 ([OWASP API1:2023](https://owasp.org/API-Security/editions/2023/en/0xa1-broken-object-level-authorization/), CWE-639),并用证据证明每一个发现,因此该报告可以直接作为合并的拦截条件,而不是引发争论。 你的测试套件以单一租户身份运行,所以那个忘了加租户过滤的查询看起来毫无问题。TenantTrace 会向你的应用中注入两个租户的数据,然后以其中一个租户的身份请求另一个租户的数据,并报告返回的结果。 ``` ✗ [critical/confirmed] Cross-tenant read on GET /api/invoices/{invoice_id} GET /api/invoices/018f4c1e-3a9b-7c2d-9e5f-1a2b3c4d5e6f 200 · body contains tt-canary-B-…3f7a91c2 ``` 这不是相似度评分或启发式判断。我们在 90 毫秒前把那段字符串植入到了租户 B 的发票中,而它却返回给了租户 A。 ## 查看运行效果 **[打开示例报告 →](https://halilibrahimd27.github.io/tenant-trace/report-leaking.html)** · [对隔离正确的应用进行的相同测试](https://halilibrahimd27.github.io/tenant-trace/report-clean.html) 这两份报告都是在每次部署时由真实工具生成的,而非截图。 ``` git clone https://github.com/halilibrahimd27/tenant-trace cd tenant-trace docker compose up -d ``` 这将启动两个多租户应用——一个是刻意制造了数据泄露的,另一个是正确隔离的——并在真实的 HTTP 和真实的 Redis 环境下对它们进行审计,然后在 **http://127.0.0.1:8088** 提供报告。无需额外安装任何东西。 报告存放在一个命名卷中,因此演示可以在所有平台上以非 root 用户身份运行。要将它们拉取到你的磁盘上:`make reports`(或者 `docker compose cp report:/reports ./reports`)。 不使用 Docker 运行: ``` uv sync --extra dev --extra fixtures uv run tenanttrace demo # audits both fixtures in-process, writes HTML ``` ## 判定原理 发现即事实,而非评分。属于租户 B 的每条记录都被植入了一个唯一的 canary 字符串。如果该 canary 字符串出现在提供给租户 A 的响应中,则该泄露被**确认** —— 无需任何主观解释。 聚合数据的工作方式也是如此:我们植入了租户 A 的数据行,所以我们知道正确的计数。任何更高的数值都是被确认的泄露。 当 oracle 无法做出决定时——例如 5xx 错误、超时、无法解析的响应体——结论将是 `inconclusive`。它永远不会默默地通过。 **每次运行都会首先检查其对照组。** 租户 A 必须能够读取租户 A 自己的数据。如果该操作失败,说明身份验证或数据植入环节出了问题,运行状态将被标记为 `INVALID` 并以退出码 3 结束。一次什么也访问不到的运行,并不等于一次什么问题都没发现的运行——这种区别正是安全工具和安慰剂之间的不同。 ## 它能捕获什么 | | | 证明方式 | | --- | --- | --- | | **对象读取** | A 通过 id 获取了 B 的记录 | 响应中包含 B 的 canary | | **集合泄露** | A 自己的列表中包含了 B 的数据行 | 响应中包含 B 的 canary | | **聚合泄露** | 跨所有租户计算的统计值 | 与植入数据行的数量进行对比 | | **参数覆盖** | `?tenant_id=B`, `X-Tenant-Id: B`, body 字段 | 在干净的基准测试之后通过 canary 确认 | | **缓存键泄露** | 查询正确,但使用了无租户隔离的缓存键 | 冷查询时被拒绝,在 B 预热缓存后被返回 | | **跨租户写入** | A 在 B 的范围内创建了记录 | 以 B 的身份读取该记录 | 缓存的情况值得停下来细看。查询是正确的——代码审查能看到标准的 `WHERE tenant_id = …`,并且单租户的测试套件也能通过——但是结果被缓存在 `invoice:{id}` 下,因此哪个租户先请求,谁就能获得数据。TenantTrace 通过冷请求该对象(被拒绝),让所有者读取它(填充缓存),然后重复第一次请求来发现这个问题。详见 [ADR-0008](docs/adr/0008-differential-attribution.md)。 ## 报告 一个完全自包含的 HTML 文件——没有 CDN、不加载字体、没有外部请求,因此可以安全地附加到工单中或在气隙隔离的机器上打开。它以结论而非方法作为开篇: - **结论。** 会明确写出 `6 confirmed cross-tenant leaks`(确认了 6 处跨租户泄露),或者 `No cross-tenant access proven — 34 attempts refused across 10 endpoints`(未证实存在跨租户访问——在 10 个端点上拒绝了 34 次尝试)。一次干净的运行会说明它覆盖了什么,因为“没有发现问题”和“什么都没测试”绝不能看起来一样。 - **访问图。** 哪个租户访问了哪个端点,仅根据已证实的结果绘制——`LEAKED`(已泄露)判定构成一条边,而 `ENFORCED`(已限制)则不构成。这个想法源自 BloodHound:在庞大的 API 中,最有价值的信息是“这一百个端点中有四个是边界被破坏的地方”,而表格无法表达这一点。 - **每一项发现及其证明请求** —— 包含 canary、响应,以及针对数据访问边界(而非路由处理器)的修复建议。 - **运行完整性置于末尾。** 包括正向对照组、应用程序拒绝的内容,以及 oracle 无法做出决定的任何内容。 `--format md` 会输出相同的内容,适用于 Pull Request;`--format json` 则适用于机器读取。在这三种格式中,凭据均会被剔除,且 canary 会被缩短。 ## 双引擎 | | | | | --- | --- | --- | | **probe** | 动态检测,语言无关 | 发送真实的请求。适用于 FastAPI、Laravel、Rails、.NET——任何使用 HTTP 通信的系统。发现的结论为 `confirmed`(已确认)。 | | **static** | 特定语言适配器 | 读取源代码以发现可疑路径,以及 HTTP 无法察觉的泄露:原生 SQL、缓存键、任务负载。发现的结论在被探测引擎证实之前均为 `suspected`(疑似)。 | 静态引擎提出假设,动态引擎进行证实。默认情况下,只有状态为 confirmed(已确认)的发现才会导致构建失败——这正是保持拦截门限可控的关键。详见 [ADR-0002](docs/adr/0002-dynamic-first-architecture.md)。 静态引擎使用标准库的 `ast` 进行解析。它绝不会导入或执行被分析的代码 ([ADR-0005](docs/adr/0005-stdlib-ast-over-tree-sitter.md))。 ## 为什么不使用你现有的工具 相关的相邻工具是存在的,但本工具并不能替代它们中的任何一个。 | | 功能 | 为什么它们无法回答这个问题 | | --- | --- | --- | | **Semgrep / CodeQL** | 对“没有租户过滤器的查询”进行模式匹配 | 仅支持静态分析,因此永远无法真正确认问题——而且面对 repository/service-layer(存储库/服务层)模式时(过滤器定义的位置往往离具体的查询很远),该规则会标记每一个调用点。导致出现成百上千的警报,却没有真正的参考价值。 | | **Burp Autorize / AuthMatrix** | 使用第二个身份重放你的请求 | 基于代理驱动:你浏览页面,它负责重放。其判断机制是*响应相似度*,这在双向上都极易产生误判,且完全无法对聚合数据进行判断。它也无法作为合并拦截门运行。 | | **Schemathesis / Dredd** | 基于 schema 对 API 进行属性测试 | 在 schema 一致性检查方面非常出色。但完全没有租户隔离的概念。 | | **你的测试套件** | 测试其他所有方面 | 仅以单一租户身份运行,因此那个忘了加过滤器的查询依然会返回看似正确的答案。 | 这里的本质区别在于: 1. **精确的判定机制。** 我们预先植入数据,随后再去查找它们,因此发现的结果非真即假——没有相似度评分,也没有阈值。 2. **静态提出假设,动态进行证实。** 假设只有在被真实请求证实后才会拦截 CI。这正是将误报率降低到足以拦截合并的程度的关键。 3. **无头运行模式。** 这是一个结合基准线文件的合并拦截门,而不是一个交互式的代理会话。 4. **跨层级检测。** 覆盖缓存键、聚合数据和后台任务负载,而不仅限于端点响应。 如果你的应用程序没有 OpenAPI 文档,可以通过一次完整操作导出 HAR 文件,然后让 TenantTrace 针对它进行检测即可。 ## 接入你的应用 TenantTrace 无法猜测你的应用程序是如何创建租户、进行身份验证或创建归属记录的。你只需编写一次(大约三十行代码)——可以从 [`seeders/example_seeder.py`](seeders/example_seeder.py) 或正在使用的 [`fixtures/seeder.py`](fixtures/seeder.py) 开始: ``` class MySeeder: def __init__(self, client): self.client = client def create_tenant(self, label): # -> {"tenant_id": ..., "access_token": ...} def auth_headers(self, tenant): # -> {"Authorization": f"Bearer {...}"} def seed_records(self, tenant, canary): # -> records carrying the canary def cleanup(self, tenant): # -> remove what you created ``` 请将 canary 放在 API 实际会返回的字段中——例如标题、名称或描述。**每种记录至少创建两条**:测试框架会将它的对照组读取和攻击读取放在不同的记录上 ([ADR-0008](docs/adr/0008-differential-attribution.md))。 然后指向它: ``` [target] base_url = "http://127.0.0.1:8000" allowed_hosts = ["127.0.0.1", "localhost"] spec = "openapi" # or "har" / "postman" / "routes" spec_path = "http://127.0.0.1:8000/openapi.json" [seeder] adapter = "seeders.my_app:MySeeder" [tenancy] column = "tenant_id" cross_tenant_allowlist = ["/api/admin/*"] # endpoints that cross tenants on purpose ``` ``` tenanttrace validate-config -c tenanttrace.toml # says exactly what it will do tenanttrace probe -c tenanttrace.toml --dry-run # lists attempts, sends nothing tenanttrace probe -c tenanttrace.toml ``` `tenanttrace.example.toml` 文档说明了每一个配置项,并且有测试断言加载器能接受它——因此它不会发生偏移。 ### 从你自己的测试套件中运行 探测引擎接受注入的传输层,因此它可以在进程内驱动 ASGI 应用程序,无需服务器、无需端口、无需容器: ``` from tenanttrace.probe.asgi import SyncASGITransport from tenanttrace.probe.runner import ProbeOptions, run_probe def test_tenants_are_isolated(): with SyncASGITransport(my_app) as transport: report = run_probe(config, ProbeOptions(transport=transport)).report assert report.status is RunStatus.VALID # controls passed — the run is real assert report.confirmed == () ``` ## 在 CI 中使用 ``` - uses: halilibrahimd27/tenant-trace@v0 with: config: tenanttrace.toml fail-on: high baseline: .tenanttrace-baseline.json ``` 已确认的发现会保存在基准线文件中并保持静默;而新出现的发现会导致测试失败。指纹能够抵御重新植入数据、端点重排、参数重命名以及代码行号变动的影响——它们是基于端点或源代码*符号*构建的,绝不依赖行号 ([ADR-0007](docs/adr/0007-baseline-fingerprints.md))。基准线文件仅保存指纹和标题:绝不包含 canary、token 或响应体。 退出码:`0` 表示干净 · `1` 表示达到了 `fail-on` 级别的发现 · `2` 表示用法或配置错误 · `3` 表示**运行状态 INVALID**,正向对照组失败。 ## 它无法发现什么 明确说明这一点也是工具可信度的一部分。 - **任何它无法植入的数据。** Oracle 之所以有效,是因为 TenantTrace 会预先植入随后要查找的数据。它无法审计你无权写入的系统。 - **没有 HTTP 接口的泄露。** 如果报告生成器将错误租户的数据行写入了无人获取的文件中,探测是无法发现的。静态引擎可以标记出该代码路径;但它无法证明存在泄露。 - **它未知的路由。** 覆盖率来源于 OpenAPI 文档、HAR 抓包、Postman 集合或手写的路由列表。未记录的端点将无法被测试,同时报告会指出它所知晓的端点数量,从而避免单薄的清单被伪装成完美的测试结果。 - **总和。** 聚合 oracle 会根据植入的数据行计数来判断 `*_count` 字段。它**不会**判断 `*_total`,因为这通常是金额,如果将其与行数进行比较,就会在代码完全正确的情况下报告出严重警报。 - **超越租户隔离的授权问题。** 查看者是否*在*单一租户内具有管理员的操作权限,这是另一个问题,本工具并不涉及。 - **静态分析非 Python 代码库。** 探测引擎是与语言无关的;静态引擎目前仅提供了一个适配器(Python + SQLAlchemy)。 ## 安全性 探测引擎会发送对抗性请求,并在启用 `--allow-mutation` 时写入数据。 - **默认只读。** 变更型攻击必须在命令行中使用 `--allow-mutation` *并且*在配置文件中设置 `allow_mutation = true`。仅设置其中一项是不够的。 - **主机白名单。** 目标主机必须存在于 `allowed_hosts` 列表中。 - **非回环地址目标额外需要使用 `--i-have-authorization`。** 该参数代表你做出的声明,而不是本工具授予你的权限。 - **不跟随重定向** —— 重定向可能会将请求发送到白名单之外的主机。 - **速率限制** 为 `max_rps`,在两个租户会话之间共享。 - **在创建记录时就对凭据进行脱敏**,而不是在渲染时处理,因此 token 绝对不可能进入产物文件中。我们通过测试确保没有任何 JWT 会进入 `exchanges.jsonl`。 - 每一个请求和响应都会被捕获并保存到 `.tenanttrace/` 目录中,该目录已被 gitignore 忽略,因为它包含真实的测试发现。 变更型攻击在完成后会自动清理产生的数据;如果未能,也会在发现报告中明确说明。 参见 [SECURITY.md](SECURITY.md) 和 [THREAT_MODEL.md](THREAT_MODEL.md)。 ## 开发 ``` make install # uv sync --extra dev --extra fixtures make verify # ruff · black · mypy --strict · pytest ≥85% · recall ≥90% make demo # audit both fixtures, write reports make fixtures-up # boot the fixtures in Docker (only needed for the HTTP demo) ``` `make verify` 是拦截门限,CI 也会运行相同的命令。它是**完全自包含的**——不需要 Docker、Redis 或网络连接——因为测试夹具是在进程内通过 ASGI 驱动的 ([ADR-0004](docs/adr/0004-hermetic-in-process-auditing.md))。 该拦截门限包含一项针对 [`fixtures/labels.yaml`](fixtures/labels.yaml) 的准确率/召回率评分,该文件是描述测试应用中所有漏洞的标准答案。如果召回率低于 90%,或者在隔离正确的应用上出现任何误报,构建就会失败。这就是将“我认为它能用”变成具体数字的方式。 [`CONTRIBUTING.md`](CONTRIBUTING.md) 包含开发规范,[`CLAUDE.md`](CLAUDE.md) 包含不可妥协的原则,而每一个重要决策都被记录在 [`docs/adr/`](docs/adr/) 中。 ## 许可证 MIT —— 详见 [LICENSE](LICENSE)。
标签:API安全, BOLA检测, CISA项目, Docker, JSON输出, 安全测试, 安全防御评估, 对称加密, 搜索引擎查询, 攻击性安全, 自动化payload嵌入, 自动化审计, 请求拦截, 逆向工具