135 lines
3.0 KiB
Python
135 lines
3.0 KiB
Python
"""
|
||
竞态条件漏洞知识
|
||
"""
|
||
|
||
from ..base import KnowledgeDocument, KnowledgeCategory
|
||
|
||
|
||
RACE_CONDITION = KnowledgeDocument(
|
||
id="vuln_race_condition",
|
||
title="Race Condition",
|
||
category=KnowledgeCategory.VULNERABILITY,
|
||
tags=["race", "condition", "toctou", "concurrency", "thread"],
|
||
severity="medium",
|
||
cwe_ids=["CWE-362", "CWE-367"],
|
||
owasp_ids=["A04:2021"],
|
||
content="""
|
||
竞态条件发生在多个操作之间存在时间窗口,攻击者可以利用这个窗口改变系统状态。
|
||
|
||
## 危险模式
|
||
|
||
### TOCTOU (Time-of-Check to Time-of-Use)
|
||
```python
|
||
# 危险 - 检查和使用之间有时间窗口
|
||
if os.path.exists(filepath): # 检查
|
||
# 攻击者可在此时替换文件
|
||
with open(filepath) as f: # 使用
|
||
data = f.read()
|
||
|
||
# 危险 - 余额检查
|
||
if user.balance >= amount: # 检查
|
||
# 并发请求可能同时通过检查
|
||
user.balance -= amount # 使用
|
||
db.commit()
|
||
```
|
||
|
||
### 双重支付/提现
|
||
```python
|
||
# 危险 - 无锁的余额操作
|
||
@app.route('/withdraw', methods=['POST'])
|
||
def withdraw():
|
||
amount = request.json['amount']
|
||
if current_user.balance >= amount:
|
||
current_user.balance -= amount
|
||
db.commit()
|
||
return transfer_money(amount)
|
||
```
|
||
|
||
### 文件操作竞态
|
||
```python
|
||
# 危险 - 临时文件
|
||
import tempfile
|
||
fd, path = tempfile.mkstemp()
|
||
# 攻击者可能在此时访问或替换文件
|
||
os.chmod(path, 0o644)
|
||
```
|
||
|
||
### 会话竞态
|
||
```python
|
||
# 危险 - 会话更新
|
||
session['cart_total'] = calculate_total()
|
||
# 并发请求可能覆盖
|
||
apply_discount(session['cart_total'])
|
||
```
|
||
|
||
## 检测要点
|
||
1. 检查-使用模式(if exists then use)
|
||
2. 余额/库存等数值操作
|
||
3. 文件创建和权限设置
|
||
4. 无锁的数据库操作
|
||
5. 会话状态修改
|
||
|
||
## 安全实践
|
||
1. 使用数据库事务和锁
|
||
2. 原子操作
|
||
3. 使用文件锁
|
||
4. 乐观锁/悲观锁
|
||
5. 幂等性设计
|
||
|
||
## 修复示例
|
||
|
||
### 数据库锁
|
||
```python
|
||
# 安全 - 使用SELECT FOR UPDATE
|
||
from sqlalchemy import select
|
||
|
||
@app.route('/withdraw', methods=['POST'])
|
||
def withdraw():
|
||
amount = request.json['amount']
|
||
|
||
with db.begin():
|
||
# 行级锁
|
||
user = db.execute(
|
||
select(User).where(User.id == current_user.id).with_for_update()
|
||
).scalar_one()
|
||
|
||
if user.balance >= amount:
|
||
user.balance -= amount
|
||
return transfer_money(amount)
|
||
else:
|
||
return "Insufficient balance", 400
|
||
```
|
||
|
||
### 原子操作
|
||
```python
|
||
# 安全 - 原子更新
|
||
from sqlalchemy import update
|
||
|
||
result = db.execute(
|
||
update(User)
|
||
.where(User.id == user_id)
|
||
.where(User.balance >= amount) # 条件更新
|
||
.values(balance=User.balance - amount)
|
||
)
|
||
if result.rowcount == 0:
|
||
return "Insufficient balance", 400
|
||
```
|
||
|
||
### 文件锁
|
||
```python
|
||
# 安全 - 使用文件锁
|
||
import fcntl
|
||
|
||
with open(filepath, 'r+') as f:
|
||
fcntl.flock(f.fileno(), fcntl.LOCK_EX)
|
||
try:
|
||
data = f.read()
|
||
# 处理数据
|
||
f.seek(0)
|
||
f.write(new_data)
|
||
finally:
|
||
fcntl.flock(f.fileno(), fcntl.LOCK_UN)
|
||
```
|
||
""",
|
||
)
|