diff --git a/backend/app/api/strategy.py b/backend/app/api/strategy.py index 67a2ce7..dc951ad 100644 --- a/backend/app/api/strategy.py +++ b/backend/app/api/strategy.py @@ -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} diff --git a/backend/app/strategy/ai_generator.py b/backend/app/strategy/ai_generator.py index 6f9429f..c80a560 100644 --- a/backend/app/strategy/ai_generator.py +++ b/backend/app/strategy/ai_generator.py @@ -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: diff --git a/backend/app/strategy/engine.py b/backend/app/strategy/engine.py index ff3330c..a031d84 100644 --- a/backend/app/strategy/engine.py +++ b/backend/app/strategy/engine.py @@ -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}") diff --git a/frontend/src/components/screener/StrategyBuilderDialog.tsx b/frontend/src/components/screener/StrategyBuilderDialog.tsx index 2da0737..f460b0f 100644 --- a/frontend/src/components/screener/StrategyBuilderDialog.tsx +++ b/frontend/src/components/screener/StrategyBuilderDialog.tsx @@ -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() diff --git a/frontend/src/components/screener/StrategyPoolDialog.tsx b/frontend/src/components/screener/StrategyPoolDialog.tsx index 8a57bd2..9eae1f5 100644 --- a/frontend/src/components/screener/StrategyPoolDialog.tsx +++ b/frontend/src/components/screener/StrategyPoolDialog.tsx @@ -119,7 +119,6 @@ export function StrategyPoolDialog({ pool, onConfirm, onClose }: Props) { code, target_source: target, mode: 'create', - strict: true, }) await loadStrategies() setActiveTab(result.source) diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 4f992a1..0d8ce2f 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -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('/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('/api/strategies/code/save', { method: 'POST',