fix(captcha): 验证码存储容量上限防 OOM (P1-1)
新增 MAX_STORE_SIZE(默认 10000,可配置 max_store/CAPTCHA_MAX_STORE), _handle_image 在清理过期项后检查容量,超限拒绝生成并返回错误, 防止攻击者高频生成验证码填满内存。
This commit is contained in:
1 parent
4d9bb5d940
commit
fe618be1fa
2 files changed
+27
-8
No files matched your search
@@ -50,7 +50,7 @@ _confs_port = int(os.environ.get('CONFS_PORT', '8010'))
|
||||
# 白名单结构:_merge_ini 仅覆盖此处列出的 section/key,防止意外字段
|
||||
_captcha_config = {
|
||||
'server': {'host': '', 'port': '', 'workers': ''},
|
||||
'captcha': {'len': '', 'ttl': '', 'font_path': ''},
|
||||
'captcha': {'len': '', 'ttl': '', 'font_path': '', 'max_store': ''},
|
||||
}
|
||||
|
||||
|
||||
@@ -98,12 +98,15 @@ if os.environ.get('WORKERS'):
|
||||
_captcha_config['server']['workers'] = os.environ['WORKERS']
|
||||
if os.environ.get('CAPTCHA_FONT_PATH'):
|
||||
_captcha_config['captcha']['font_path'] = os.environ['CAPTCHA_FONT_PATH']
|
||||
if os.environ.get('CAPTCHA_MAX_STORE'):
|
||||
_captcha_config['captcha']['max_store'] = os.environ['CAPTCHA_MAX_STORE']
|
||||
|
||||
# 配置完整性校验
|
||||
_required = {
|
||||
'server': ['host', 'port', 'workers'],
|
||||
'captcha': ['len', 'ttl'],
|
||||
}
|
||||
# max_store 是可选配置(非必填)
|
||||
for section, keys in _required.items():
|
||||
for key in keys:
|
||||
if not _captcha_config[section][key]:
|
||||
@@ -115,6 +118,14 @@ for section, keys in _required.items():
|
||||
CAPTCHA_LEN = int(_captcha_config['captcha']['len'])
|
||||
CAPTCHA_TTL = int(_captcha_config['captcha']['ttl'])
|
||||
|
||||
# 验证码存储容量上限(可选配置,默认 10000 个),防止内存耗尽(审计 P1-1)
|
||||
_default_max_store = '10000'
|
||||
MAX_STORE_SIZE = int(_captcha_config['captcha']['max_store'] or _default_max_store)
|
||||
if MAX_STORE_SIZE <= 0:
|
||||
print('[captcha] 致命错误: 配置项 [captcha] max_store 必须为正整数',
|
||||
file=_sys.stderr)
|
||||
_sys.exit(1)
|
||||
|
||||
# 字体路径:可选配置项(缺失或用空值时不致命,回退到代码默认值;
|
||||
# 即使默认值文件不存在,PIL 在 truetype() 失败时会回退到 load_ Default())。
|
||||
DEFAULT_FONT_PATH = '/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf'
|
||||
@@ -207,13 +218,21 @@ class CaptchaServer:
|
||||
token = secrets.token_hex(16)
|
||||
|
||||
with STORE_LOCK:
|
||||
captcha_store[token] = {'answer': text.lower(), 'created_at': time.time()}
|
||||
# 惰性清理过期项
|
||||
now = time.time()
|
||||
expired = [k for k, v in captcha_store.items() if now - v['created_at'] > CAPTCHA_TTL]
|
||||
for k in expired:
|
||||
del captcha_store[k]
|
||||
|
||||
# 容量上限保护(P1-1):清理过期后仍超限则拒绝新生成,防止内存耗尽
|
||||
if len(captcha_store) >= MAX_STORE_SIZE:
|
||||
logs.warning("captcha",
|
||||
f"验证码存储已满({len(captcha_store)}/{MAX_STORE_SIZE}),拒绝生成")
|
||||
return {"success": False,
|
||||
"error": "too many pending captchas, please try again later"}
|
||||
|
||||
captcha_store[token] = {'answer': text.lower(), 'created_at': now}
|
||||
|
||||
png_bytes = generate_image(text)
|
||||
logs.info("captcha", f"验证码已生成 token={token[:8]}...")
|
||||
return {
|
||||
|
||||
@@ -16,15 +16,15 @@
|
||||
| 等级 | 总数 | 已修复 | 部分修复 | 未修复 | 修复率 |
|
||||
|------|------|--------|----------|--------|--------|
|
||||
| P0 | 0 | 0 | 0 | 0 | - |
|
||||
| P1 | 1 | 0 | 0 | 1 | 0% |
|
||||
| P1 | 1 | 1 | 0 | 0 | 100% |
|
||||
| P2 | 1 | 0 | 0 | 1 | 0% |
|
||||
| **合计** | **2** | **0** | **0** | **2** | **0%** |
|
||||
| **合计** | **2** | **1** | **0** | **1** | **50%** |
|
||||
|
||||
### 逐条验证
|
||||
|
||||
| 编号 | 状态 | 代码位置 | 验证说明 |
|
||||
|------|------|----------|----------|
|
||||
| P1-1 | ❌ 未修复 | `captcha_service.py` L128-129, L204-222 | `captcha_store` dict 无容量上限,`_handle_image` 无生成速率限制,大量请求可耗尽内存 |
|
||||
| P1-1 | ✅ 已修复 | `captcha_service.py` L227-232 | 新增 `MAX_STORE_SIZE`(默认 10000,可配置 `max_store`/`CAPTCHA_MAX_STORE`),`_handle_image` 在加锁后先清理过期项,再检查容量,超限拒绝生成并返回错误,防止内存耗尽 |
|
||||
| P2-1 | ❌ 未修复 | `captcha_service.py` L128 | 仍为进程内存 dict,未迁移至 Redis;当前单实例可接受 |
|
||||
|
||||
---
|
||||
@@ -34,7 +34,7 @@
|
||||
| 优先级 | 总数 | 已修复 | 待修复 |
|
||||
|--------|------|--------|--------|
|
||||
| P0 | 0 | 0 | 0 |
|
||||
| P1 | 1 | 0 | 1 |
|
||||
| P1 | 1 | 1 | 0 |
|
||||
| P2 | 1 | 0 | 1 |
|
||||
|
||||
---
|
||||
@@ -43,7 +43,7 @@
|
||||
|
||||
| # | 问题 | 说明 |
|
||||
|---|------|------|
|
||||
| P1-1 | 验证码存储无容量限制,大量生成可耗尽内存 | 无验证码数量上限,攻击者可高频调用 `/captcha/image` 生成大量验证码填满内存 |
|
||||
| P1-1 | ~~验证码存储无容量限制,大量生成可耗尽内存~~ | ✅ 已修复(2026-08-04):新增 `MAX_STORE_SIZE`(默认 10000),`_handle_image` 清理过期后容量仍满则拒绝生成并返回 `too many pending captchas`,防止内存耗尽 |
|
||||
|
||||
---
|
||||
|
||||
@@ -94,7 +94,7 @@
|
||||
## 修复路线图
|
||||
|
||||
### 短期
|
||||
- [ ] 添加验证码存储容量上限(如最多 10000 个,超出时清理最早的)
|
||||
- [x] 添加验证码存储容量上限(默认 10000,超出时拒绝新生成)✅ 已修复(2026-08-04)
|
||||
- [ ] 添加生成速率限制(单 IP 每分钟最多生成 N 个)
|
||||
- [ ] 将 `generate_text()` 中的 `random.choices` 替换为 `secrets` 模块
|
||||
|
||||
|
||||
Reference in new issue
Block a user