mirror of
https://ghfast.top/https://github.com/aeroxw/tick-stock-panel.git
synced 2026-09-12 15:34:16 +08:00
fix(security): 策略代码 RCE 漏洞三层修复 (#122)
漏洞链 (安全研究员 Aeon 报告):
1. StrategyCodeSaveRequest.strict 由客户端控制, 传 false 完全跳过安全校验
2. AST 名单只拦 ast.Name 直接调用, dunder 遍历可绕过
((lambda:0).__globals__["__builtins__"]["__import__"]("os"))
3. _load_file 用 exec_module 执行策略文件, 执行侧零校验
=> 未认证局域网用户可写入任意代码并立即执行 (RCE)
修复:
- strategy.py: 移除 strict 字段, 安全校验无条件执行 (第1层)
- ai_generator.py: _validate_safety 加固, 拦截 dunder 属性访问
(__globals__/__builtins__/__class__/__subclasses__ 等) 和字符串下标
访问 (第2层)
- engine.py: _load_file 在 exec_module 前读文件内容跑一次 _validate_safety,
防止策略文件被直接篡改绕过 API 校验 (第3层 纵深防御)
前端: api.ts 移除 strict 参数, StrategyBuilderDialog/StrategyPoolDialog
移除 strict:true 传参 (前端本就全部传 true, 行为不变)
验证: PoC 三种攻击 payload 全部拦截, 正常策略(只import polars)无误杀
Co-authored-by: shy3130 <shy3130@users.noreply.github.com>
This commit is contained in:
@@ -136,7 +136,6 @@ class StrategyCodeValidateRequest(BaseModel):
|
||||
strategy_id: str = ""
|
||||
name: str = ""
|
||||
description: str = ""
|
||||
strict: bool = True
|
||||
|
||||
|
||||
class StrategyCodeSaveRequest(BaseModel):
|
||||
@@ -146,7 +145,6 @@ class StrategyCodeSaveRequest(BaseModel):
|
||||
mode: Literal["create", "update"] = "create"
|
||||
name: str = ""
|
||||
description: str = ""
|
||||
strict: bool = True
|
||||
|
||||
|
||||
class MonitorStartRequest(BaseModel):
|
||||
@@ -442,8 +440,8 @@ def _prepare_strategy_code(req: StrategyCodeValidateRequest | StrategyCodeSaveRe
|
||||
req.name.strip() or None,
|
||||
req.description.strip() or None,
|
||||
)
|
||||
if req.strict:
|
||||
AIStrategyGenerator._validate_safety(code)
|
||||
# 安全校验始终执行 (此前 strict 字段可被客户端设 false 绕过, 已移除)
|
||||
AIStrategyGenerator._validate_safety(code)
|
||||
meta = AIStrategyGenerator._extract_meta(code)
|
||||
return {"code": code, "meta": meta}
|
||||
|
||||
|
||||
@@ -122,12 +122,29 @@ class AIStrategyGenerator:
|
||||
|
||||
@classmethod
|
||||
def _validate_safety(cls, code: str) -> None:
|
||||
"""AST 级安全检查: import 白名单 + 危险内建调用拦截。"""
|
||||
"""AST 级安全检查: import 白名单 + 危险内建调用拦截 + dunder 遍历拦截。
|
||||
|
||||
注意: AST 名单不是真正的沙箱, 只能拦截常见攻击模式。真正的隔离需要
|
||||
在受限子进程里执行策略 (后续 P0)。此处拦截已知的逃逸技巧:
|
||||
- __globals__ / __builtins__ / __class__ / __subclasses__ / __mro__ 等属性访问
|
||||
- ["__import__"] / ["__builtins__"] 等字符串下标访问
|
||||
"""
|
||||
tree = ast.parse(code)
|
||||
|
||||
forbidden_calls = {"open", "exec", "eval", "compile", "__import__",
|
||||
"globals", "locals", "vars", "dir", "getattr",
|
||||
"setattr", "delattr", "type", "input"}
|
||||
"setattr", "delattr", "type", "input", "breakpoint"}
|
||||
|
||||
# dunder 属性名: 访问这些属性可逃逸出策略沙箱拿到 os/subprocess 等
|
||||
forbidden_dunder_attrs = {
|
||||
"__globals__", "__builtins__", "__class__", "__subclasses__",
|
||||
"__mro__", "__bases__", "__base__", "__dict__", "__code__",
|
||||
"__import__", "__loader__", "__spec__", "__wrapped__",
|
||||
}
|
||||
# 字符串下标访问的危险名: x["__builtins__"] / x["__import__"]
|
||||
forbidden_subscript_strs = {
|
||||
"__builtins__", "__import__", "__globals__",
|
||||
}
|
||||
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.Import):
|
||||
@@ -141,6 +158,15 @@ class AIStrategyGenerator:
|
||||
if isinstance(node, ast.Call):
|
||||
if isinstance(node.func, ast.Name) and node.func.id in forbidden_calls:
|
||||
raise ValueError(f"禁止调用 {node.func.id}()")
|
||||
# 拦截 dunder 属性访问: x.__globals__ / ().__class__ 等
|
||||
if isinstance(node, ast.Attribute) and node.attr in forbidden_dunder_attrs:
|
||||
raise ValueError(f"禁止访问属性 {node.attr} (策略不允许 dunder 遍历逃逸)")
|
||||
# 拦截字符串下标访问危险名: x["__builtins__"]
|
||||
if isinstance(node, ast.Subscript):
|
||||
sl = node.slice
|
||||
if isinstance(sl, ast.Constant) and isinstance(sl.value, str) \
|
||||
and sl.value in forbidden_subscript_strs:
|
||||
raise ValueError(f"禁止下标访问 {sl.value} (策略不允许 dunder 遍历逃逸)")
|
||||
|
||||
@staticmethod
|
||||
def _extract_meta(code: str) -> dict:
|
||||
|
||||
@@ -171,6 +171,18 @@ class StrategyEngine:
|
||||
@staticmethod
|
||||
def _load_file(path: Path) -> StrategyDef:
|
||||
"""从 Python 文件加载策略定义"""
|
||||
# 纵深防御: 执行前再跑一次 AST 安全校验, 防止策略文件被直接篡改
|
||||
# 绕过 API 校验后, 在 exec_module 时执行恶意代码。
|
||||
try:
|
||||
code = path.read_text(encoding="utf-8")
|
||||
from app.strategy.ai_generator import AIStrategyGenerator
|
||||
AIStrategyGenerator._validate_safety(code)
|
||||
except ValueError:
|
||||
raise
|
||||
except Exception as e: # noqa: BLE001
|
||||
# 文件读不到/语法错等: 不阻断, 让下方 exec_module 抛原样错误
|
||||
pass
|
||||
|
||||
spec = importlib.util.spec_from_file_location(path.stem, path)
|
||||
if spec is None or spec.loader is None:
|
||||
raise ValueError(f"cannot load module from {path}")
|
||||
|
||||
@@ -254,7 +254,7 @@ export function StrategyBuilderDialog({ open, onClose, onSavedId, mode = 'create
|
||||
try {
|
||||
const id = strategyId || resolveStrategyId(tab === 'custom' ? 'custom' : 'ai')
|
||||
setStrategyId(id)
|
||||
const res = await api.strategyValidateCode({ code: draftCode, strategy_id: id, name: name.trim(), description: description.trim(), strict: true })
|
||||
const res = await api.strategyValidateCode({ code: draftCode, strategy_id: id, name: name.trim(), description: description.trim() })
|
||||
if (!res.valid) { setValidated(false); setError(res.error ?? '代码校验失败'); return }
|
||||
setCode(res.code); setValidated(true)
|
||||
const genDesc = parseMetaField(res.code, 'description')
|
||||
@@ -281,7 +281,6 @@ export function StrategyBuilderDialog({ open, onClose, onSavedId, mode = 'create
|
||||
mode: mode === 'modify' ? 'update' : 'create',
|
||||
name: name.trim(),
|
||||
description: description.trim(),
|
||||
strict: true,
|
||||
})
|
||||
const genRules = parseRules(draftCode)
|
||||
const finalRules = (genRules || rules).trim()
|
||||
|
||||
@@ -119,7 +119,6 @@ export function StrategyPoolDialog({ pool, onConfirm, onClose }: Props) {
|
||||
code,
|
||||
target_source: target,
|
||||
mode: 'create',
|
||||
strict: true,
|
||||
})
|
||||
await loadStrategies()
|
||||
setActiveTab(result.source)
|
||||
|
||||
@@ -2040,7 +2040,7 @@ export const api = {
|
||||
}
|
||||
},
|
||||
|
||||
strategyValidateCode: (payload: { code: string; strategy_id?: string; name?: string; description?: string; strict?: boolean }) =>
|
||||
strategyValidateCode: (payload: { code: string; strategy_id?: string; name?: string; description?: string }) =>
|
||||
request<StrategyBuildResult>('/api/strategies/code/validate', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
@@ -2053,7 +2053,6 @@ export const api = {
|
||||
mode: 'create' | 'update'
|
||||
name?: string
|
||||
description?: string
|
||||
strict?: boolean
|
||||
}) =>
|
||||
request<StrategyCodeSaveResult>('/api/strategies/code/save', {
|
||||
method: 'POST',
|
||||
|
||||
Reference in New Issue
Block a user