Prhub

#6542 [rollout] fix: avoid arbitrary code execution in Qwen3 tool parser

原始 PR 作者 fuergaosi233 合并时间 2026-06-07 17:49 文件变更 2 提交数 1 评论 1 代码增减 +87 / -2

执行摘要

修复 Qwen3 工具解析器中 eval 导致的 RCE 漏洞

PR body 明确指出:Qwen3XMLToolParser._parse_xml_function_callarray 等非基本类型参数值调用 eval(param_value)param_value 直接来自解码后的模型输出,恶意构造的工具调用可导致任意代码执行(RCE)。关联 issue #5331 跟踪同类漏洞并推荐 ast.literal_eval()

值得合并。修复关键安全漏洞,代码简洁,测试完备。建议开发者关注 ast.literal_eval 的限制(不支持复杂表达式),但当前场景足够。

讨论亮点

无 review 评论或争议。gemini-code-assist[bot] 自动审查后表示无反馈。wuxibin89 直接批准。

实现拆解

  1. 导入调整:在 verl/experimental/agent_loop/tool_parser.py 顶部新增 import ast
  2. 替换 eval 调用:在 convert_param_value 函数的 else 分支中,将 param_value = eval(param_value) 替换为 param_value = ast.literal_eval(param_value),并更新日志消息中提及的转换方式。
  3. 新增 CPU 测试:创建 tests/experimental/agent_loop/test_qwen3_tool_parser_on_cpu.py,包含两个测试用例:
    • test_array_literal_is_parsed:验证合法数组字面量 [1, 2, 3] 仍被正确解析。
    • test_array_param_does_not_execute_arbitrary_code:验证代码执行载荷被安全拒绝并降级为原始字符串。
      测试通过模拟工具模式、直接调用 _parse_xml_function_call,无需 GPU 或 tokenizer。
文件 模块 状态 重要度
tests/experimental/agent_loop/test_qwen3_tool_parser_on_cpu.py 工具解析 added 7.38
verl/experimental/agent_loop/tool_parser.py 工具解析 modified 6.41

关键符号

convert_param_value

关键源码片段

tests/experimental/agent_loop/test_qwen3_tool_parser_on_cpu.py test-coverage

新增 CPU 测试,覆盖合法数组解析和 RCE 防护,确保修复正确性。

# 测试文件:tests/experimental/agent_loop/test_qwen3_tool_parser_on_cpu.py
"""CPU tests for ``Qwen3XMLToolParser`` parameter-value conversion.These exercise ``_parse_xml_function_call`` directly (no tokenizer / network
needed) and in particular guard against arbitrary code execution when a
non-primitive parameter (e.g. an ``array``) is parsed from untrusted model
output.
"""import jsonfrom verl.experimental.agent_loop.tool_parser import Qwen3XMLToolParser
from verl.tools.schemas import (
    OpenAIFunctionParametersSchema,
    OpenAIFunctionPropertySchema,
    OpenAIFunctionSchema,
    OpenAIFunctionToolSchema,
)
​
​
def _make_tool(param_name: str, param_type: str) -> OpenAIFunctionToolSchema:
    # 根据参数名和类型构建一个测试用的工具 schema
    return OpenAIFunctionToolSchema(
        type="function",
        function=OpenAIFunctionSchema(
            name="list_tool",
            description="a tool used for testing",
            parameters=OpenAIFunctionParametersSchema(
                type="object",
                properties={param_name: OpenAIFunctionPropertySchema(type=param_type)},
                required=[],
            ),
        ),
    )
​
​
def _parse_single_param(param_type: str, raw_value: str):
    # 构造一个完整的 XML 函数调用字符串,模拟模型输出中的 <function=...><parameter=...>...</parameter>...</function>
    parser = Qwen3XMLToolParser(tokenizer=None)
    tool = _make_tool("items", param_type)
    function_call_str = f"list_tool><parameter=items>{raw_value}</parameter>"
    result = parser._parse_xml_function_call(function_call_str, [tool])
    return result
​
​
def test_array_literal_is_parsed():
    """A legitimate array literal is still parsed into its Python value."""
    result = _parse_single_param("array", "[1, 2, 3]")
    # 合法的列表字面量应被正确解析为 JSON 数组
    assert result.arguments == '{"items": [1, 2, 3]}'
​
​
def test_array_param_does_not_execute_arbitrary_code(tmp_path):
    """An ``array`` parameter must never execute code from model output.    Regression test: the value used to be passed to ``eval()``, allowing
    arbitrary code execution. It must now be handled by ``ast.literal_eval``,
    which rejects non-literals and degenerates to the raw string.
    """
    marker = tmp_path / "pwned.txt"
    assert not marker.exists()
​
    # 构造一个可能导致代码执行的 payload
    payload = f'__import__("os").system("echo pwned > {marker}")'
    result = _parse_single_param("array", payload)
​
    # 验证没有代码执行:标记文件未被创建
    assert not marker.exists(), "ast.literal_eval must not execute arbitrary code"
    # 验证不可解析的值被降级为原始字符串
    assert json.loads(result.arguments)["items"] == payload
verl/experimental/agent_loop/tool_parser.py security-fix

核心修复文件:将 eval 替换为 ast.literal_eval,修复 RCE 漏洞。

# 文件:verl/experimental/agent_loop/tool_parser.py
# 在文件顶部新增 import
import ast# ... 其他代码 ...def convert_param_value(param_value: str, param_name: str, param_config: dict, func_name: str):
    # 根据参数类型转换值,处理基本类型(string/int/float/bool)后,对于其他类型(如 array)使用 ast.literal_eval
    # ... 前面的类型判断代码 ...
    else:
        if param_type == "object" or param_type.startswith("dict"):
            try:
                param_value = json.loads(param_value)
                return param_value
            except Exception:
                logger.warning(
                    f"Parsed value '{param_value}' of parameter '{param_name}' is not a valid "
                    f"JSON object in tool '{func_name}', will try other methods to parse it."
                )
        try:
            # 使用 ast.literal_eval 替代 eval:参数值来自不可信的模型输出,
            # eval() 会允许任意代码执行。literal_eval 只解析 Python 字面量
            # (列表、元组、字典、数字等)。
            param_value = ast.literal_eval(param_value)
        except Exception:
            logger.warning(
                f"Parsed value '{param_value}' of parameter '{param_name}' cannot be converted "
                f"via `ast.literal_eval()` in tool '{func_name}', degenerating to string."
            )
        return param_value

评论区精华

没有提炼出高价值讨论线程

当前评论区没有形成足够清晰的争议点或结论,后续有更多讨论时会体现在这里。

风险与影响

变更非常集中(仅 2 个文件,源码 +6/-2),风险极低。ast.literal_evaleval 的 fallback 行为一致(异常时返回原始字符串),不会破坏现有功能。测试覆盖了合法和恶意输入,回归风险小。

影响所有使用 Qwen3XMLToolParser 的 agent 工作流,消除 RCE 漏洞,提升系统安全性。用户无感知,合法功能(如解析数组参数)保持不变。

安全修复

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论