From 373045f0a3378ef186f81ddb6cb7f9c3f7ffa0a8 Mon Sep 17 00:00:00 2001 From: qing <1776177662@qq.com> Date: Mon, 10 Aug 2026 01:08:55 +0800 Subject: [PATCH] =?UTF-8?q?pymsg:=20=E5=B0=86=E5=B7=B2=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E5=AE=89=E5=85=A8=E5=AE=A1=E8=AE=A1=E9=A1=B9=E5=90=88=E5=85=A5?= =?UTF-8?q?=E6=BA=90=E7=A0=81=E6=B3=A8=E9=87=8A,=20=E7=B2=BE=E7=AE=80?= =?UTF-8?q?=E5=AE=A1=E8=AE=A1=E6=8A=A5=E5=91=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 P0-1/P0-2/P1-1/P1-2/P1-3/P1-5 共 6 条已修复发现作为 [SECURITY AUDIT ... ✅ 已修复] 注释合入 pymsg 源码对应位置 - 重写 pymsg 审计报告, 删除已合并条目, 仅保留 10 条未修复项待跟踪 --- .../security/pymsg_SECURITY_AUDIT_20260801.md | 138 +++++++----------- pymsg/pymsg/__init__.py | 38 ++++- pymsg/pymsg/rpc.py | 15 +- 3 files changed, 99 insertions(+), 92 deletions(-) diff --git a/docs/security/pymsg_SECURITY_AUDIT_20260801.md b/docs/security/pymsg_SECURITY_AUDIT_20260801.md index d92161f..0fcf21e 100644 --- a/docs/security/pymsg_SECURITY_AUDIT_20260801.md +++ b/docs/security/pymsg_SECURITY_AUDIT_20260801.md @@ -6,33 +6,44 @@ --- -## 代码验证记录 +## ⚠️ 合并追溯说明(2026-08-10) -**验证日期**: 2026-08-03 -**验证方式**: 逐条对照审计报告与当前代码(`__init__.py`、`rpc.py`、`async_queue.py`) +原始审计报告共 16 条发现(P0×3 / P1×5 / P2×4 / P3×4)。其中 **6 条已修复项** +(P0-1、P0-2、P1-1、P1-2、P1-3、P1-5)的修复代码已落地并经 2026-08-03 验证, +现已作为 `[SECURITY AUDIT ... ✅ 已修复]` 注释合并进源文件对应位置: -### 验证结果汇总 +| 编号 | 已合并位置 | +|------|------------| +| P0-1 | `pymsg/__init__.py` — `recv()`、`_recv_exact()` | +| P0-2 | `pymsg/__init__.py` — `recv()`、`_handle_client()` | +| P1-1 | `pymsg/__init__.py` — `MsgServer.__init__`、`_accept_client()` | +| P1-2 | `pymsg/__init__.py` — `_BoundedThreadPoolExecutor` 类、`MsgServer.__init__`、`_handle_client()` | +| P1-3 | `pymsg/rpc.py` — `_sanitize_error()` | +| P1-5 | `pymsg/rpc.py` — 模块顶部 `_MAX_NUM_DIGITS` 注释、`_dispatch()` | -| 等级 | 总数 | 已修复 | 部分修复 | 未修复 | 修复率 | -|------|------|--------|----------|--------|--------| -| P0 | 3 | 2 | 0 | 1 | 67% | -| P1 | 5 | 4 | 0 | 1 | 80% | -| P2 | 4 | 0 | 0 | 4 | 0% | -| P3 | 4 | 0 | 0 | 4 | 0% | -| **合计** | **16** | **6** | **0** | **10** | **37.5%** | +**本报告以下章节仅保留尚未修复(未合并)的 10 条发现作为待办跟踪**,已修复条目 +已从「逐条验证」「修复进度」及详细问题章节中删除,其完整描述见源代码注释。 -### 逐条验证 +--- -| 编号 | 状态 | 代码位置 | 验证说明 | -|------|------|----------|----------| -| P0-1 | ✅ 已修复 | `__init__.py` L103-120, L140-170 | `recv()` 改为 deadline 模式:`_recv_exact` 接收 `time.monotonic()+timeout` 作为总截止时刻,每次 `recv_into` 前按剩余时间收紧 socket 超时;header 与 payload 共享同一总超时,逐字节发送无法刷新 deadline,半开连接被及时关闭(已实测:0.6s 超时 0.60s 即抛 TimeoutError) | -| P0-2 | ✅ 已修复 | `__init__.py` L113-114, L462-463 | 双重防护:`recv()` 将 `UnicodeDecodeError` 包装为 `ConnectionError`;`_handle_client` 防御性捕获 `UnicodeDecodeError` | +## 剩余未修复条目(待跟踪) + +### 汇总 + +| 等级 | 剩余总数 | 说明 | +|------|----------|------| +| P0 | 1 | 无认证/鉴权机制 | +| P1 | 1 | 明文 TCP,无 TLS | +| P2 | 4 | 健壮性/配置问题 | +| P3 | 4 | 优化建议 | +| **合计** | **10** | | + +### 逐条验证(仅未合并项) + +| 编号 | 状态 | 代码位置 | 说明 | +|------|------|----------|------| | P0-3 | ❌ 未修复 | `rpc.py` L80-171 | 无认证机制,仅依赖 127.0.0.1 网络层隔离 | -| P1-1 | ✅ 已修复 | `__init__.py` L342-350, L467-484 | `MsgServer` 新增 `max_connections` 参数(默认 1000),`_accept_client` 在 `len(_clients) >= max_connections` 时直接拒绝并关闭新连接;已实测 max_connections=3 时第 4 个连接被关闭(ConnectionResetError),handler 只处理前 3 条 | -| P1-2 | ✅ 已修复 | `__init__.py` L316-333, L369, L405-410, L530-540 | 新增 `_BoundedThreadPoolExecutor`(`submit` 时检查 `_work_queue.qsize()`,超限抛 `queue.Full`);`MsgServer` 新增 `max_queue_size` 参数(默认 1000),`_handle_client` 捕获 `queue.Full` 后拒绝并断开该连接;已实测 workers=1 + max_queue_size=1 时,慢 handler 阻塞下第 3 个连接被拒绝关闭,handler 只处理前 2 个任务 | -| P1-3 | ✅ 已修复 | `rpc.py` L49-57, L160-178 | 新增 `_sanitize_error()`:用 `_PATH_RE` 正则将异常信息中的绝对路径(如 `/opt/micro_servers/db/xxx.py`)替换为 ``,保留业务错误语义;`_dispatch` 返回给客户端前统一调用;已实测异常信息含绝对路径时返回体仅含 ``,不透传 `/opt/...` | | P1-4 | ❌ 未修复 | 全模块 | 明文 TCP,无 TLS 支持 | -| P1-5 | ✅ 已修复 | `rpc.py` L28-46, L158-166 | `json.loads` 增加 `parse_int=_checked_int` / `parse_float=_checked_float` 钩子,数字位数超 `_MAX_NUM_DIGITS=100` 抛 `ValueError`;`except` 捕获 `RecursionError` 防止深度嵌套导致栈溢出;解析失败统一返回 `"invalid JSON request"` 不透传输入片段;已实测超大整数(10000 位)与深度 200000 嵌套均被拒绝,服务存活 | | P2-1 | ❌ 未修复 | `__init__.py` L106-111 | length=0 空消息无特殊处理 | | P2-2 | ❌ 未修复 | `__init__.py` L395-418 | `broadcast()` 无频率/权限控制 | | P2-3 | ❌ 未修复 | `__init__.py` L377-378 | `except (ValueError, OSError): break` 仍捕获所有 OSError(含 EINTR) | @@ -55,44 +66,20 @@ --- -## 修复进度 +## 修复进度(剩余) -| 优先级 | 总数 | 已修复 | 待修复 | -|--------|------|--------|--------| -| P0 | 3 | 2 | 1 | -| P1 | 5 | 2 | 3 | -| P2 | 4 | 0 | 4 | -| P3 | 4 | 0 | 4 | +| 优先级 | 剩余待修复 | +|--------|------------| +| P0 | 1(无认证) | +| P1 | 1(无 TLS) | +| P2 | 4 | +| P3 | 4 | --- ## 🔴 P0 — 严重问题 -### P0-1: recv 超时可被绕过(Slowloris 变种 DoS)✅ 已修复 -- **位置**: `__init__.py:103-120`(`recv`)、`__init__.py:140-170`(`_recv_exact`) -- **原问题**: `sock.settimeout()` 设置的是**单次系统调用**超时,不是整个消息接收总超时。攻击者可每次在快超时时发送 1 字节: - - 4 字节长度头最长可占用 `4 * recv_timeout = 40秒` - - 载荷部分按字节发送可使连接存活任意长时间 -- **影响**: 少量攻击者即可耗尽连接/线程资源,造成所有依赖 pymsg 的服务拒绝服务 -- **修复方案(deadline 模式)**: - - `recv()` 在开头计算 `deadline = time.monotonic() + timeout`,header 与 payload 共享**同一总截止时刻** - - `_recv_exact` 每次 `recv_into` 前按 `deadline - now` 设置剩余 socket 超时,剩余时间耗尽即抛 `TimeoutError` - - 逐字节发送会不断刷新单次 `sock.settimeout()`,但**无法刷新总 deadline**,半开连接会被及时关闭 - - 实测验证:超时 0.6s 时,逐字节攻击在 0.60s 即被 TimeoutError 终止(旧实现需等到完整消息 1.4s) - ---- - -### P0-2: UnicodeDecodeError 可导致同步模式服务崩溃 ✅ 已修复 -- **位置**: `__init__.py:113-114`、`__init__.py:462-463` -- **原问题**: `payload.decode("utf-8")` 抛出的 `UnicodeDecodeError`(`ValueError` 子类)不在 `_handle_client` 的异常捕获列表中,同步模式下收到无效 UTF-8 字节会直接导致整个服务进程崩溃 -- **修复方案(双重防护)**: - 1. `recv()` 函数将 `UnicodeDecodeError` 包装为 `ConnectionError`(第一层) - 2. `_handle_client` 异常捕获列表添加 `UnicodeDecodeError`(第二层,防御性兜底) -- **影响范围**: 已消除,db/confs/smtp/captcha 等所有 pymsg 服务均受保护 - ---- - -### P0-3: 无认证/鉴权机制(已部分缓解:绑定 127.0.0.1) +### P0-3: 无认证/鉴权机制 - **位置**: `rpc.py:80-171` 整个 RpcServer - **问题**: RPC 服务完全没有身份验证,任何能连接到端口的客户端都能调用所有路由 - **当前缓解**: 所有服务均绑定 127.0.0.1,依赖网络层隔离 @@ -107,11 +94,7 @@ | # | 问题 | 位置 | 状态 | |---|------|------|------| -| P1-1 | 无连接数限制,连接耗尽攻击 | `__init__.py:342-350,467-484` | ✅ 已修复(`max_connections` 默认 1000) | -| P1-2 | 线程池无队列限制,慢 handler 耗尽内存 | `__init__.py:316-333,369,405-410,530-540` | ✅ 已修复(`_BoundedThreadPoolExecutor` + `max_queue_size` 默认 1000) | -| P1-3 | 异常信息直接返回客户端,泄露内部细节 | `rpc.py:49-57,160-178` 新增 `_sanitize_error()` 脱敏绝对路径 | ✅ 已修复 | | P1-4 | 无 TLS/加密传输,内网流量可被窃听 | 全模块明文 TCP | ❌ 未修复 | -| P1-5 | JSON 解析无深度限制,深度嵌套导致栈溢出/CPU 100% | `rpc.py:28-46,158-166` `parse_int/parse_float` 钩子 + 捕获 `RecursionError` | ✅ 已修复 | --- @@ -141,39 +124,22 @@ 1. ✅ 64 MiB 消息大小上限(`_MAX_MSG_SIZE`),防止 OOM 2. ✅ send/recv 双端都检查消息大小 -3. ✅ recv_timeout 防 Slowloris(基础实现,需修复 P0-1) -4. ✅ 线程池模式下同一连接串行处理,避免帧交错 -5. ✅ 使用 `sendmsg` 散射-聚集 I/O 减少内存拷贝 -6. ✅ 使用 `recv_into` 预分配 buffer -7. ✅ 线程安全的发送锁 -8. ✅ 回调异常隔离(线程池模式) -9. ✅ 单元测试覆盖消息大小边界 +3. ✅ 线程池模式下同一连接串行处理,避免帧交错 +4. ✅ 使用 `sendmsg` 散射-聚集 I/O 减少内存拷贝 +5. ✅ 使用 `recv_into` 预分配 buffer +6. ✅ 线程安全的发送锁 +7. ✅ 回调异常隔离(线程池模式) +8. ✅ 单元测试覆盖消息大小边界 -## 新增安全机制(审计后添加,审计报告未覆盖) - -| 机制 | 代码位置 | 说明 | -|------|----------|------| -| UnicodeDecodeError 双重防护 | `__init__.py` L113-114, L462-463 | `recv()` 包装为 `ConnectionError` + `_handle_client` 防御性捕获,防止无效 UTF-8 导致服务崩溃 | -| AsyncRpcQueue 有界队列 | `async_queue.py` L47, L130-132 | `max_queue_size` 默认 10000,超限丢弃最旧数据,防止内存无限增长 | -| AsyncRpcQueue 错误隔离 | `async_queue.py` L49, L189-194 | `on_error` 回调 + 异常捕获,后台线程失败不影响主业务 | -| RpcClient 响应格式校验 | `rpc.py` L89-90 | 校验响应必须是含 `success` 字段的 dict,非法响应抛出 `RpcError` | -| MsgClient.call 节制日志 | `__init__.py` L209, L248-254 | 重试日志首次立即打印,后续每 5 秒一次,避免刷屏 | +> 注:P0-1(recv 总超时)、P0-2(UnicodeDecodeError 防护)、P1-1(连接数限制)、 +> P1-2(有界队列)、P1-3(异常脱敏)、P1-5(JSON 解析限制)等已修复机制均已合并进 +> 源代码注释,见开头「合并追溯说明」。 --- -## 修复路线图 - -### 立即(24小时) -- [x] 修复 P0-1: recv 总超时(deadline 模式)✅ 已完成 -- [x] 修复 P0-2: 捕获 UnicodeDecodeError/JSONDecodeError ✅ 已完成 - -### 短期(72小时) -- [x] 添加最大连接数限制 ✅ 已完成 -- [x] 线程池改用有界队列 ✅ 已完成 -- [x] 异常信息脱敏 ✅ 已完成 -- [x] JSON 解析深度限制 ✅ 已完成 +## 修复路线图(剩余) ### 中期 -- [ ] 统一认证机制 -- [ ] TLS 支持选项 -- [ ] 安全事件日志 +- [ ] 统一认证机制(P0-3) +- [ ] TLS 支持选项(P1-4) +- [ ] 安全事件日志(P3-1) diff --git a/pymsg/pymsg/__init__.py b/pymsg/pymsg/__init__.py index 849d239..5438a4f 100644 --- a/pymsg/pymsg/__init__.py +++ b/pymsg/pymsg/__init__.py @@ -101,6 +101,12 @@ def recv( message is received. TimeoutError: If the operation exceeds the timeout. """ + # [SECURITY AUDIT P0-1 ✅ 已修复] recv 总超时(deadline 模式) + # 原问题:sock.settimeout() 是「单次系统调用」超时而非整条消息总超时, + # 攻击者可每次在快超时时发 1 字节绕过,造成 Slowloris 型连接耗尽 DoS。 + # 修复:header 与 payload 共享同一总截止时刻 deadline,每次 recv_into + # 前按剩余时间收紧 socket 超时,半开连接被及时关闭(实测 0.6s 超时即终止)。 + # 详见 docs/security/pymsg_SECURITY_AUDIT_20260801.md deadline = None if timeout is not None: deadline = time.monotonic() + timeout @@ -113,6 +119,11 @@ def recv( ) payload = _recv_exact(sock, length, deadline) return payload.decode("utf-8") + # [SECURITY AUDIT P0-2 ✅ 已修复] UnicodeDecodeError 导致同步服务崩溃 + # 原问题:payload.decode("utf-8") 抛出的 UnicodeDecodeError(ValueError 子类) + # 不在 _handle_client 捕获列表中,同步模式收到无效 UTF-8 会令整个进程崩溃。 + # 修复(双重防护):此处将 UnicodeDecodeError 包装为 ConnectionError(第一层), + # _handle_client 再防御性捕获兜底(第二层)。db/confs/smtp/captcha 等服务均受保护。 except UnicodeDecodeError: raise ConnectionError("invalid UTF-8 payload") from None except (TimeoutError, _socket.timeout): @@ -155,6 +166,9 @@ def _recv_exact( ``TimeoutError``。这防止 Slowloris 型攻击:逐字节发送会不断刷新单次 ``sock.settimeout()``,但无法刷新总 deadline,半开连接会被及时关闭。 """ + # [SECURITY AUDIT P0-1 ✅ 已修复] 本函数为 P0-1 的核心修复实现, + # 与上方 recv() 的 deadline 计算配合,构成整条消息的总超时防护。 + # 详见 docs/security/pymsg_SECURITY_AUDIT_20260801.md buf = bytearray(n) view = memoryview(buf) got = 0 @@ -313,6 +327,11 @@ class MsgClient: self._sock = None +# [SECURITY AUDIT P1-2 ✅ 已修复] 线程池无队列限制 → 慢 handler 耗尽内存 +# 原问题:标准 ThreadPoolExecutor 使用无界队列,慢 handler 时任务无限堆积直至 OOM。 +# 修复:本类在 submit 时检查排队深度,超限即抛 queue.Full;MsgServer 新增 +# max_queue_size 参数(默认 1000),_handle_client 捕获 queue.Full 后拒绝并断开连接。 +# 详见 docs/security/pymsg_SECURITY_AUDIT_20260801.md class _BoundedThreadPoolExecutor(ThreadPoolExecutor): """有界线程池:提交队列有上限,满时抛 ``queue.Full``。 @@ -368,7 +387,13 @@ class MsgServer: def __init__(self, addr: str, port: int, *, workers: int = 0, recv_timeout: float = _RECV_TIMEOUT, + # [SECURITY AUDIT P1-1 ✅ 已修复] 无连接数限制 → 连接耗尽攻击 + # 原问题:服务端无并发连接上限,攻击者可耗尽文件描述符/线程资源。 + # 修复:新增 max_connections(默认 1000),_accept_client 在 + # len(_clients) >= max_connections 时直接拒绝并关闭新连接。 + # 详见 docs/security/pymsg_SECURITY_AUDIT_20260801.md max_connections: int = 1000, + # [SECURITY AUDIT P1-2 ✅ 已修复] 见上方 _BoundedThreadPoolExecutor max_queue_size: int = 1000) -> None: self.addr = addr self.port = port @@ -499,8 +524,9 @@ class MsgServer: client_sock, client_addr = servsock.accept() if client_sock.fileno() == -1: return - # 活跃连接数上限控制:超过 max_connections 直接拒绝, - # 防止连接耗尽攻击(listen backlog 只限制 SYN 队列,不限制已建立连接数) + # [SECURITY AUDIT P1-1 ✅ 已修复] 活跃连接数上限控制:超过 + # max_connections 直接拒绝,防止连接耗尽攻击(listen backlog 只限制 + # SYN 队列,不限制已建立连接数)。本段即 P1-1 的落地实现。 if len(self._clients) >= self._max_connections: try: client_sock.close() @@ -518,8 +544,9 @@ class MsgServer: text = recv(s, timeout=self._recv_timeout) except (ConnectionError, ConnectionResetError, OSError, TimeoutError, UnicodeDecodeError): - # TimeoutError: 客户端在超时时间内未发送完整消息(Slowloris 防护),关闭连接 - # UnicodeDecodeError: 防御性捕获,recv() 已包装为 ConnectionError,此处兜底 + # [SECURITY AUDIT P0-2 ✅ 已修复(第二层兜底)] UnicodeDecodeError: + # recv() 已将其包装为 ConnectionError,此处防御性捕获防止任何遗漏。 + # TimeoutError:客户端在超时时间内未发送完整消息(Slowloris 防护,P0-1),关闭连接 self._remove_client(s, socketlist) return addr = self._clients.get(s, ("unknown", 0)) @@ -537,7 +564,8 @@ class MsgServer: try: self._pool.submit(self._run_task, s, addr, text, on_message) except _queue.Full: - # 有界队列已满:慢 handler 时任务不应无限堆积,拒绝并断开该连接 + # [SECURITY AUDIT P1-2 ✅ 已修复] 有界队列已满:慢 handler 时任务不应 + # 无限堆积,拒绝并断开该连接,防止内存无限增长。 with self._lock: self._busy.discard(s) self._remove_client(s, socketlist) diff --git a/pymsg/pymsg/rpc.py b/pymsg/pymsg/rpc.py index 8ebbc5e..0d8913c 100644 --- a/pymsg/pymsg/rpc.py +++ b/pymsg/pymsg/rpc.py @@ -25,6 +25,13 @@ from . import MsgClient, MsgServer __all__ = ["RpcClient", "RpcServer", "RpcError"] +# [SECURITY AUDIT P1-5 ✅ 已修复] JSON 解析无深度/数字位数限制 → 栈溢出 / CPU 100% +# 原问题:json.loads 对深度嵌套与超大数字无限制,恶意输入可触发 RecursionError +# 栈溢出或消耗大量 CPU/内存。修复:parse_int/parse_float 钩子限制数字位数 +# <= _MAX_NUM_DIGITS(100),并捕获 RecursionError 防止深度嵌套;解析失败统一返回 +# "invalid JSON request" 不透传输入片段(见 _dispatch)。实测 10000 位整数与 +# 深度 200000 嵌套均被拒绝,服务存活。 +# 详见 docs/security/pymsg_SECURITY_AUDIT_20260801.md # 超大数字位数上限:防止超大整数/浮点(如 10^100000)消耗大量 CPU 与内存 _MAX_NUM_DIGITS = 100 @@ -46,6 +53,11 @@ def _checked_float(value: str) -> float: return float(value) +# [SECURITY AUDIT P1-3 ✅ 已修复] 异常信息直接返回客户端 → 泄露内部细节 +# 原问题:handler 抛出的异常 str(e) 含绝对路径(如 /opt/micro_servers/db/xxx.py)、 +# 堆栈等内部细节,直接回显给客户端。修复:_dispatch 返回前统一经 _sanitize_error() +# 将绝对路径替换为 ,保留业务错误语义(实测含绝对路径时仅返回 )。 +# 详见 docs/security/pymsg_SECURITY_AUDIT_20260801.md def _sanitize_error(msg: str) -> str: """脱敏异常信息中的绝对路径等内部细节,保留业务错误语义。 @@ -187,7 +199,8 @@ class RpcServer: # ── 分发与响应包装 -------------------------------------------------- def _dispatch(self, sock, addr, text): try: - # parse_int/parse_float 限制超大数字;RecursionError 防护深度嵌套 + # [SECURITY AUDIT P1-5 ✅ 已修复] parse_int/parse_float 限制超大数字; + # RecursionError 防护深度嵌套(落地实现见文件顶部 _MAX_NUM_DIGITS 注释) req = json.loads(text, parse_int=_checked_int, parse_float=_checked_float) except (json.JSONDecodeError, ValueError, RecursionError):