用 Agent 缩减攻击面:三遍扫描各查什么
原文:Anthropic Research - 《Using Claude to reduce the attack surface of code》(https://www.anthropic.com/research/attack-surface-reduction)
安全团队做代码审计,习惯的起手式是"找漏洞"。但前沿红队这篇(Chris Rohlf,2026 年 10 月 20 日)走的是另一条路:不找具体漏洞,而是减少代码被外部够到的路径。原文那句判断值得先抄下来——减少不必要的攻击面,等于一次性降低了这段代码里所有未知漏洞的风险,包括已经在被悄悄利用的那些。
对做 Agent 的人来说,这篇的价值不在结论,而在于它公开了一条可复用的流水线:怎么把一个"帮我审代码"的模糊需求,拆成三遍各管一件事的确定性任务。
三遍扫描的分工
原文把缩减攻击面拆成三种方法,工具链就跑三遍,一遍对应一种。
默认配置可达:找出在出厂默认配置下就能被触达、但没有任何默认功能打算使用的代码。处理方式是给它加一道门(gate),把这部分能力挪到网关之后。
早期可达性:找出在认证这类硬网关之前执行的代码,或者在任何配置下都跑在协议状态机入口之前的代码,以及不需要受害者交互就能触发的路径。处理方向是把 pre-auth 挪到 post-auth、0-click 挪到 1-click、默认开启改成显式 opt-in。
配置交互:找出那些单独看都合理、配在一起就出问题的选项对。典型症状是静默禁掉了某个安全控制,或者某个功能的行为已经未定义、但它的代码依然可达。
三种方法的共同点很关键:它们找的都不是"代码写错了",而是"代码放错了位置"或者"两个正确选项组合出了错误状态"。这类问题不会被常规的 SAST 规则命中,因为单看每一行都合法。
一条流水线怎么搭
原文的工程实现只有一个可复用 skill,正在合入它家的 OSS-Scanner。跑法是三遍串行,每一遍都从空上下文开始,只接收上一遍传过来的一小段结论。这个设计不是省 token,是为了防止前一遍的判断污染后一遍的视野——同一个人(或同一个上下文)既找默认配置问题又找配置组合问题,很容易把后一遍做成前一遍的重复。
每一遍内部的动作是固定的两步:先让模型从代码里整理出入口点清单,再对每个候选做一次动态追踪,确认它真的能被够到。追踪不到的直接丢掉,这一步替代了大部分人工复核。
只对默认配置那一类结果做了自动化改动,而且加了一道很硬的约束:
PASSES = [
# 第一遍:默认配置下可达、但没有默认功能会用的入口
("default", "列出默认配置可达、却没有任何默认功能使用的入口点"),
# 第二遍:任何配置下都跑在认证/协议状态机入口之前的代码
("early", "列出在任何配置下都先于硬网关或协议状态机入口执行的代码"),
# 第三遍:单项合理、组合起来静默削弱安全控制的选项对
("combo", "列出单独合理但组合后会静默关闭安全控制的配置项对"),
]
def run_passes(repo, carry="(无)"):
results = []
for name, instruction in PASSES:
# 每遍都开新上下文,只带上一遍的一句话结论
findings = agent.scan(repo, instruction, prior=carry)
kept = []
for f in findings:
trace = dynamic_trace(repo, f["entry"]) # 动态追踪确认可达
if trace.reached: # 够不到就丢弃
kept.append({**f, "trace": trace.path})
results.append((name, kept))
carry = summarize(kept) # 只把摘要交给下一遍
return results
def guard(findings, repo):
# 只给默认配置类发现加门;加门后默认行为必须一模一样
baseline = run_tests(repo) # 项目自带测试套件
patched = run_tests(repo, mutate=add_gate(findings))
blackbox = run_regression(repo) # 黑盒回归套件
if baseline != patched or not blackbox.ok:
return "退回人工" # 有任何差异就不自动改
return "可提交"
这段是我按原文描述重写的示意代码,不是官方实现,接口名都是自拟的。看几个关键位置:run_passes 的 prior 参数只吃一句摘要,作用是让第二遍知道第一遍已经排除了哪些入口,而不是让它继承判断标准;dynamic_trace 是这套方法里最不该省的一环,静态看到的"可达"经常是假的;guard 的判断顺序是先跑基线测试再跑改后测试,两次结果必须完全一致才算过——注意原文的要求是"identical results",不是"没有新失败",这个区别在大型项目里会筛掉大量候选改动。
原文还补了一句容易被忽略的边界:任何会改变非默认配置行为的门,一律搁到一边等人判断。另外两遍的处理方式更轻,只提出改法——调整配置检查顺序、加状态顺序的守卫、在配置加载时直接拒绝这个组合——不动代码。
25 个项目跑出来的数字
规模先摆出来:25 个开源项目、超过 500 万行代码,语言覆盖 C、C++、Java、PHP、Python。模型先建了一份超过 17,000 个配置项的清单,其中 3,842 个与安全控制或功能配置相关。在这个底盘上,三种方法一共找出 493 个问题。
项目按信任边界分了三类:10 个 public(服务任何可达用户或消费不可识别方数据,比如 DNS 解析器)、11 个 gated(只服务已认证用户,认证前不该有任何功能)、4 个 private(部署在运营者私有环境,认证层可选)。gated 这一类结果最出人意料,单这一类就产出了 93 个默认配置下不必要的预认证面、37 处认证前执行代码或记录状态的位置、61 个静默削弱管理员所配置预认证控制的组合。
配置交互那一类跨全部 25 个项目统计:162 个安全敏感组合被接受时既没有报错也没有警告,文档里也查不到。其中 94 例是静默关闭了安全控制,剩下 68 例是同一套安全机制内部的不一致——规则失效、顺序依赖之类。
最能说明"这类问题能潜伏多久"的是两个细节:一个特定模式在 4 个互不相关的项目里独立出现,成因都是关闭反向 DNS 后主机名规则 fail open;而那组互相冲突的配置选项,在软件里存在了 21 年。
OpenSSH 的案例把收益量化了。10.4 版本里约 4,500 行为 ML-DSA-44 新增的实验性代码,在默认配置下未认证客户端就能到达,这个体量约占默认 sshd 自身预认证代码的 16%。10.6 的修复移除了约五分之一此前未认证客户端可达的代码。删掉的不是漏洞,是入口。
这套方法适合谁抄
从上面的分布能看出适用面:如果你的项目有"认证之前能跑到什么"这类明确的信任边界,gated 那一类方法直接可用;如果你是库或中间件,默认配置可达那一类收益最大,因为下游全是照默认配置部署的。
不适合的场景也很清楚。原文没有给误报率数据,验证靠的是动态追踪加测试套件一致性比对,这要求项目本身有一套跑得起来的测试。没有测试套件的仓库,guard 这一层直接失效,只能退化成人工逐个看。
局限与自查清单
原文自己承认得很干脆:这些技术不移除代码里的未知漏洞,只是让它们更难被够到,从而降低严重性;对攻击面缩减这件事,本文只提供了一个非常有限的视角。它还提到一个没展开的方向——用运行时日志和配置做比对,确认只开启了真正需要的功能。
真要落地,我建议按下面四条先跑一轮小的:
- 先挑一个 gated 型服务,只跑早期可达性这一遍。这一类判据最客观(认证点在哪是明确的),人工复核成本最低。
- 入口点清单必须落成表格,字段包含入口、触发条件、是否需要交互、追踪结论。没有这张表,后面所有讨论都会回到"我觉得这里危险"。
- 默认配置类的改动一律走测试套件比对,且要求完全一致。放宽成"没有新增失败",等于自己给自己发通行证。
- 配置交互这一遍单独跑,别和默认配置混在一起。混跑的典型后果是把 162 个组合问题写成几条泛泛的"建议检查配置组合"。
具体配置项、扫描器接口和 skill 的接入方式以官方文档为准。但方法本身可以直接用:把"审代码"拆成三遍互不干扰的确定性任务,比让 Agent 一次性做全量审计要靠谱得多。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/2401_88139521/article/details/168765999




