ParshwaBhavsar/raceguard
GitHub: ParshwaBhavsar/raceguard
基于 AST 的 Python 静态分析工具,专门检测 TOCTOU 竞态条件和并发安全 bug 并输出 SARIF 报告。
Stars: 0 | Forks: 0
# raceguard




**用于发现 Python 源代码中 TOCTOU 竞态条件和并发安全 bug 的静态分析工具。** `raceguard` 会将你的代码解析为抽象语法树 (AST),并标记出“检查后执行”模式 —— 例如读取余额后进行单独写入、检查唯一性后进行插入、检查文件系统存在性后再打开、未加锁的读取-修改-写入 —— 然后会针对每种情况解释正确的修复方法,并映射到 CWE。
它只负责读取源代码并进行报告。它绝不执行其分析的代码,不发送任何网络流量,也不做任何更改 —— 这与任何 linter 或 SAST 工具的契约相同。
## 目录
- [存在的意义](#why-this-exists)
- [检测内容](#what-it-detects)
- [安装](#install)
- [快速开始](#quick-start)
- [用法](#usage)
- [输出示例](#example-output)
- [工作原理](#how-it-works)
- [“检查后执行”模式](#the-check-then-act-patterns)
- [CI 集成](#ci-integration)
- [测试](#testing)
- [项目结构](#project-structure)
- [展示的概念](#concepts-demonstrated)
- [局限性](#limitations)
- [道德与范围](#ethics--scope)
## 存在的意义
竞态条件是应用程序安全中测试最不充分的漏洞类型之一。它们隐藏在看似普通的代码中 —— 读取一个值、检查它、对它执行操作 —— 直到有两个请求同时到达时才会表现出异常。接着,钱包会发生双花,一次性优惠券被重复兑换,库存出现超卖,或者“唯一”的电子邮箱被注册两次。它们在正常测试中很少出现,因为只有在并发情况下才会显现,而且传统的 linter 不会去查找它们。
`raceguard` 将安全防护左移。它不是试图在运行的服务器前*抢占先机*赢得竞态(这是一种攻击性方法),而是静态地读取源代码,并在代码发布之前,直接指出那些在构造上就容易产生竞态的“检查后执行”模式。这是开发人员或审查者在代码审查期间在自己的代码库上运行的工具,用于捕获 TOCTOU bug。
这是防御性工具包的源代码层:基于 AST 的静态分析,作为在运行时分析二进制文件、日志和网络流量工具的补充。
## 检测内容
| 检查 ID | 模式 | 严重程度 | CWE |
|----------|---------|----------|-----|
| `RG-DB-CHECK-THEN-ACT` | 读取 DB 行 → 根据其值进行条件判断 → 单独写入,无锁/原子更新(双花、透支、优惠券重用) | HIGH | CWE-367 |
| `RG-CHECK-THEN-CREATE` | 查询存在性 → 如果不存在则插入,无唯一约束/upsert(记录重复) | HIGH | CWE-367 |
| `RG-FS-CHECK-THEN-USE` | `os.path.exists()`/`access()` → 单独的 `open()`/使用(符号链接 TOCTOU) | MEDIUM | CWE-367, CWE-362 |
| `RG-NONATOMIC-COUNTER` | 在锁外部对共享的实例状态执行 `+=`/`-=`(库存超卖、丢失递减) | MEDIUM | CWE-362 |
| `RG-UNGUARDED-RMW` | `self.x = self.x + …` 对共享状态进行读取-修改-写入,未持有锁 | MEDIUM | CWE-362 |
每个发现都包含文件/行/列号、有问题的代码片段、关于该竞态的通俗易懂的英语解释,以及具体的修复方案。
## 安装
```
git clone https://github.com/ParshwaBhavsar/raceguard.git
cd raceguard
pip install -r requirements.txt
pip install -e .
```
需要 Python 3.10+。运行时依赖项:`rich`。分析仅使用标准库的 `ast` 模块。
## 快速开始
```
# 审计故意存在竞争风险的示例
python -m raceguard audit samples/vulnerable.py
# 审计正确加锁的示例(报告零发现)
python -m raceguard audit samples/fixed.py
```
## 用法
```
# 审计单个文件
python -m raceguard audit path/to/module.py
# 递归审计整个项目
python -m raceguard audit path/to/src/
# 用于自动化的 JSON 输出
python -m raceguard audit src/ --format json
# 用于 GitHub code scanning 的 SARIF
python -m raceguard audit src/ --format sarif > raceguard.sarif
# CI 门控 —— 如果存在任何 HIGH+ 发现则以非零状态退出
python -m raceguard audit src/ --fail-on high
# 列出 checks
python -m raceguard list-checks
```
## 输出示例
```
raceguard — TOCTOU / concurrency audit
1 file(s) scanned under samples/vulnerable.py
╭─ 🟠 HIGH Database check-then-act race (TOCTOU) ─────────────────────────────╮
│ samples/vulnerable.py:13:4 │
│ 'account' is read from the database, then a write occurs inside a │
│ conditional that depends on it, with no row lock or atomic conditional │
│ update. Two concurrent executions can both pass the check before either │
│ writes — enabling double-spend, overdraft, or lost-update bugs. │
│ │
│ Fix: Lock the row when reading: │
│ `session.query(Account).with_for_update().filter_by(...)`; or use an atomic │
│ conditional update: `UPDATE accounts SET balance = balance - :amt WHERE …` │
│ │
│ RG-DB-CHECK-THEN-ACT CWE-367 │
╰──────────────────────────────────────────────────────────────────────────────╯
def withdraw(session, account_id, amount):
account = session.query(Account).filter_by(id=account_id).first() # READ
if account.balance >= amount: # CHECK
account.balance -= amount # ACT
session.commit()
```
`fixed.py` 示例 —— 即使用 `with_for_update()` 加锁、原子更新、`IntegrityError` 处理、EAFP 文件访问以及 `threading.Lock` 编写的相同函数 —— 报告了**零发现**。识别标准修复方案是保持该工具可用性的关键:一个对正确代码报警的 linter 最终会被关掉。
## 工作原理
```
.py file / directory
│
▼
ast.parse standard-library AST — understands scope,
│ statement order, and call structure
▼
checks/
├── db_check_then_act.py read-var → if(attr) → write, minus locks/atomics
├── check_then_create.py existence check → create; + filesystem TOCTOU
└── inmemory.py augassign / RMW on self-rooted shared state
│
▼
model.py Finding (severity, CWE, remediation, snippet)
│
▼
report.py rich terminal / JSON / SARIF
```
基于 **AST 而不是对文本进行正则匹配**,正是使得该分析具有可信度的原因。例如,数据库的“检查后执行”探测器会追踪哪些局部变量是从未加锁的 ORM 读取中赋值的,然后寻找一个 `if` 语句,其条件检查该变量的某个*属性*(如 `account.balance >= amount` 的值检查),并在其主体中跟随一个写入操作 —— 同时如果存在 `with_for_update()`、原子条件 `update()` 或周围的锁/事务上下文,则会抑制该发现报告。这种结构上的理解,正是它能够区分真正的“检查后执行”(`if account.balance >= amount`)与“检查后创建”(`if not existing`)的原因,也是它不去干涉正确加锁代码的原因。
## “检查后执行”模式
**数据库“检查后执行”**是经典的 TOCTOU。读取一行,根据其值进行分支判断,然后在单独的语句中写入。两个并发事务都读取了旧值,都通过了检查,并且都进行了写入 —— 结果导致双花。修复方法是使用行锁(`SELECT … FOR UPDATE`)、原子条件更新(`UPDATE … WHERE balance >= :amt`)或可串行化隔离。
**“检查后创建”**会查询现有的记录,如果不存在则插入。两个请求都看到“不存在”,于是两者都进行了插入。修复方法是使用数据库的 `UNIQUE` 约束并处理 `IntegrityError`,或者使用 upsert。
**文件系统“检查后使用”**先调用 `os.path.exists()` 然后再调用 `open()`。在这两步之间,另一个进程可以替换该路径(符号链接攻击)。修复方法是使用 EAFP(直接打开并捕获异常),或者使用原子标志(`O_CREAT | O_EXCL`)。
**非原子计数器 / 未受保护的读取-修改-写入**在没有锁的情况下修改了共享的内存状态(`self.stock[sku] -= 1`,`self.count = self.count + 1`)。读取-修改-写入操作在不同线程间发生交叉并导致更新丢失。修复方法是使用锁、原子原语或数据存储端的原子操作。
## CI 集成
```
# .github/workflows/raceguard.yml
- name: Concurrency-safety audit
run: |
pip install raceguard
raceguard audit src/ --fail-on high
# 或将 SARIF 上传到 GitHub code scanning:
- run: raceguard audit src/ --format sarif > raceguard.sarif
- uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: raceguard.sarif
```
## 测试
```
pip install pytest
pytest -q
```
```
22 passed in 0.09s
```
每个检查都有一个正向测试(它必须捕获的模式)和一个负向测试(它必须放过的相应*已修复*代码),此外还有一个回归测试,确保存在性检查被报告为“检查后创建”,而不会被重复报告为“检查后执行”。端到端测试断言易受攻击的示例会产生发现报告,而已修复的示例不会产生任何报告。
## 项目结构
```
raceguard/
├── raceguard/
│ ├── model.py # Finding, AuditReport, Category, CWE map
│ ├── astutils.py # AST helpers: call names, lock detection, ORM shapes
│ ├── auditor.py # parse + run checks over files/dirs
│ ├── report.py # terminal / JSON / SARIF
│ ├── cli.py # audit / list-checks
│ └── checks/
│ ├── base.py
│ ├── db_check_then_act.py # the core DB TOCTOU
│ ├── check_then_create.py # duplicate-insert + filesystem TOCTOU
│ └── inmemory.py # counters + read-modify-write
├── samples/
│ ├── vulnerable.py # one bug per function
│ └── fixed.py # correctly synchronised → 0 findings
├── tests/
│ └── test_raceguard.py # 22 tests
├── requirements.txt
├── pyproject.toml
└── README.md
```
## 展示的概念
- **基于 AST 的静态分析** —— 通过 Python 的 `ast` 模块推断作用域、语句顺序和调用结构,而不是脆弱的文本匹配
- **TOCTOU 机制** —— 跨数据库、文件系统和内存状态的“检查后执行”模式
- **并发控制** —— 识别标准修复方案:行锁(`SELECT FOR UPDATE`)、原子条件更新、可串行化隔离、唯一约束、锁以及原子原语(CWE-362, CWE-367)
- **误报控制工程** —— 在存在锁/事务/原子保护时抑制报告,从而让正确的代码保持安静(这是一个可用的 linter 和一个被忽视的 linter 之间的区别)
- **SARIF 输出** —— 用于 GitHub 代码扫描集成的标准 SAST 交换格式
- **CI 安全门禁** —— 基于严重程度阈值的退出代码,用于执行安全左移
## 局限性
- **过程内分析** —— 分析仅在函数内部进行。一个函数中的检查和另一个函数中的执行(跨越调用边界)不会被关联起来。这是快速 linter 的标准范围;全程序数据流是一项繁重得多的工作。
- **启发式 ORM 检测** —— 数据库检查通过结构和命名来识别 SQLAlchemy 风格的查询/提交模式。不常见的 ORM 包装器可能无法匹配。相关模式已记录在案,以便进行扩展。
- **无法证明并发性** —— 与任何静态工具一样,它会标记*如果*在并发下运行容易产生竞态的代码;它无法证明给定的路径确实在线程下运行。发现结果是审查候选项目,并且每个结果都附带了推理过程,供人工确认。
- **抑制机制基于保护机制** —— 它识别常见的锁/事务/原子形式。奇特的自定义锁抽象可能需要添加到 `astutils.is_lock_context` 中。
## 道德与范围
专为防御性用途构建:代码审查、安全开发和安全教育。`raceguard` 对您提供的源代码执行静态分析,不执行任何代码,也不进行任何网络连接或代码修改。仅分析您被授权审查的代码。
## 许可证
MIT
标签:Python, SAST, TOCTOU漏洞, 云安全监控, 安全规则引擎, 并发安全, 无后门, 盲注攻击, 自动化payload嵌入, 逆向工具, 静态分析