fix(pymsg): 异常信息脱敏与JSON深度/大数字限制,防信息泄露与DoS
This commit is contained in:
1 parent
87f209af73
commit
dd4fef6dee
3 files changed
+85
-13
No files matched your search
+40
-4
@@ -19,11 +19,43 @@ JSON 编解码客户端逻辑,收敛为 ``RpcServer`` / ``RpcClient`` 两个
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
|
||||
from . import MsgClient, MsgServer
|
||||
|
||||
__all__ = ["RpcClient", "RpcServer", "RpcError"]
|
||||
|
||||
# 超大数字位数上限:防止超大整数/浮点(如 10^100000)消耗大量 CPU 与内存
|
||||
_MAX_NUM_DIGITS = 100
|
||||
|
||||
# 匹配绝对路径(如 /opt/micro_servers/db/xxx.py),用于脱敏异常信息
|
||||
_PATH_RE = re.compile(r"(?<![\w])/[A-Za-z0-9_.\-]+(?:/[A-Za-z0-9_.\-]+)*")
|
||||
|
||||
|
||||
def _checked_int(value: str) -> int:
|
||||
"""parse_int 钩子:限制超大整数,防止 CPU/内存 DoS。"""
|
||||
if len(value) > _MAX_NUM_DIGITS:
|
||||
raise ValueError("integer too large")
|
||||
return int(value)
|
||||
|
||||
|
||||
def _checked_float(value: str) -> float:
|
||||
"""parse_float 钩子:限制超大浮点,防止 CPU/内存 DoS。"""
|
||||
if len(value) > _MAX_NUM_DIGITS:
|
||||
raise ValueError("number too large")
|
||||
return float(value)
|
||||
|
||||
|
||||
def _sanitize_error(msg: str) -> str:
|
||||
"""脱敏异常信息中的绝对路径等内部细节,保留业务错误语义。
|
||||
|
||||
不返回函数堆栈、SQL 等内部细节给客户端;完整异常由 ``on_error``
|
||||
钩子记录到服务端日志。
|
||||
"""
|
||||
if not msg:
|
||||
return msg
|
||||
return _PATH_RE.sub("<redacted>", msg)
|
||||
|
||||
|
||||
class RpcError(Exception):
|
||||
"""远端返回 ``success=false`` 的业务错误,或响应不符合 RPC 约定。"""
|
||||
@@ -155,9 +187,12 @@ class RpcServer:
|
||||
# ── 分发与响应包装 --------------------------------------------------
|
||||
def _dispatch(self, sock, addr, text):
|
||||
try:
|
||||
req = json.loads(text)
|
||||
except json.JSONDecodeError as e:
|
||||
self._reply(sock, False, error=f"JSON 解析失败: {e}")
|
||||
# parse_int/parse_float 限制超大数字;RecursionError 防护深度嵌套
|
||||
req = json.loads(text, parse_int=_checked_int,
|
||||
parse_float=_checked_float)
|
||||
except (json.JSONDecodeError, ValueError, RecursionError):
|
||||
# 不向客户端回显解析细节(可能含输入片段),统一脱敏文案
|
||||
self._reply(sock, False, error="invalid JSON request")
|
||||
return
|
||||
|
||||
route = req.pop("_route", "")
|
||||
@@ -175,7 +210,8 @@ class RpcServer:
|
||||
self._on_error(route, e)
|
||||
except Exception:
|
||||
pass
|
||||
self._reply(sock, False, error=str(e))
|
||||
# 脱敏后返回:保留业务错误语义,但过滤绝对路径等内部细节
|
||||
self._reply(sock, False, error=_sanitize_error(str(e)))
|
||||
|
||||
def _reply(self, sock, success, data=None, error=None):
|
||||
resp = {"success": success}
|
||||
|
||||
+37
-1
@@ -92,7 +92,7 @@ class TestRpc(unittest.TestCase):
|
||||
from pymsg import recv
|
||||
resp = json.loads(recv(s, timeout=5))
|
||||
self.assertFalse(resp["success"])
|
||||
self.assertIn("JSON 解析失败", resp["error"])
|
||||
self.assertIn("invalid JSON request", resp["error"])
|
||||
# 服务仍然存活
|
||||
self.assertEqual(RpcClient("127.0.0.1", fx.port).call("ok"), "fine")
|
||||
|
||||
@@ -133,6 +133,42 @@ class TestRpc(unittest.TestCase):
|
||||
self.assertEqual(results, [i * 2 for i in range(6)])
|
||||
self.assertLess(elapsed, 1.2, f"workers=4 应并发,实际 {elapsed:.2f}s")
|
||||
|
||||
def test_error_path_is_redacted(self):
|
||||
"""P1-3: 异常信息中的绝对路径应被脱敏,不透传内部细节。"""
|
||||
|
||||
def handler(req):
|
||||
raise ValueError("open /opt/micro_servers/db/db_tools.py: no such file")
|
||||
|
||||
with _RpcFixture({"boom": handler}) as fx:
|
||||
with self.assertRaises(RpcError) as cm:
|
||||
RpcClient("127.0.0.1", fx.port).call("boom")
|
||||
msg = str(cm.exception)
|
||||
self.assertIn("<redacted>", msg)
|
||||
self.assertNotIn("/opt/micro_servers", msg)
|
||||
|
||||
def test_huge_integer_rejected(self):
|
||||
"""P1-5: 超大整数应被拒绝,防止 CPU/内存 DoS。"""
|
||||
with _RpcFixture({"ok": lambda req: "fine"}) as fx:
|
||||
huge = "9" * 10000
|
||||
with socket.create_connection(("127.0.0.1", fx.port), timeout=5) as s:
|
||||
send(s, '{"_route": "ok", "n": %s}' % huge)
|
||||
from pymsg import recv
|
||||
resp = json.loads(recv(s, timeout=5))
|
||||
self.assertFalse(resp["success"])
|
||||
|
||||
def test_deeply_nested_json_rejected(self):
|
||||
"""P1-5: 深度嵌套 JSON 应被拒绝,防止栈溢出。"""
|
||||
with _RpcFixture({"ok": lambda req: "fine"}) as fx:
|
||||
depth = 200000
|
||||
payload = "[" * depth + "]" * depth
|
||||
with socket.create_connection(("127.0.0.1", fx.port), timeout=5) as s:
|
||||
send(s, '{"_route": "ok", "n": %s}' % payload)
|
||||
from pymsg import recv
|
||||
resp = json.loads(recv(s, timeout=5))
|
||||
self.assertFalse(resp["success"])
|
||||
# 服务仍然存活
|
||||
self.assertEqual(RpcClient("127.0.0.1", fx.port).call("ok"), "fine")
|
||||
|
||||
def test_per_call_timeout_override(self):
|
||||
with _RpcFixture({"slow": lambda req: time.sleep(2)}) as fx:
|
||||
rpc = RpcClient("127.0.0.1", fx.port, timeout=10.0)
|
||||
|
||||
Reference in new issue
Block a user