你的编码代理能过 91% 的测试,却只有 25% 的代码能安全上线
💡 工具推荐: 正则测试器、JSON 校验器、Base64 编解码
2026 年 10 月 10 日,新加坡国立大学和阿里巴巴的团队把一个我们感受了一年的事情量化了。Last Secure Code Benchmark(LSCBench)取 150 个真实漏洞,构造出 450 个任务,让六个前沿编码代理根据「只讲功能」的规格去写实现。结果是:功能测试通过率 73.6% 到 91.3%,而最强的 Claude Fable 5 只有 25.1% 的产出既跑得通又安全,其余五个在 16.4% 到 24.7% 之间。单看漏洞场景,最好的模型也就过了 12%。这不是一篇模型评测,它描述的是:当任务里没人提安全时,会发生什么。
屏幕上显示着生成代码的编辑器
一、LSCBench 到底测了什么
450 个任务横跨七种语言——Java、C、C++、JavaScript、Python、PHP、Go——以及 24 个 CWE 类别,其中包括 2025 年 CWE Top 25 中的 21 个、Top 10 KEV 弱点中的 8 个。每个漏洞被切成三种粒度:填一个函数(平均生成 155 行)、写一个完整文件(1,782 行)、从零搭出一棵仓库源码树(4,120 行)。评分由隐藏的功能测试和安全测试完成,而且两套测试都在漏洞版与修复版代码上验证过。对你工作方式最要紧的一点是:每个任务只写功能要求,对漏洞保持中立。没人告诉代理,那个压包导入器会收到一个精心构造的 tar 包。
# semgrep-rules/agent-defaults.yml
# Three rules that catch the failures agents repeat most often.
rules:
- id: python-yaml-unsafe-load
languages: [python]
severity: ERROR
message: yaml.load without SafeLoader runs arbitrary constructors.
patterns:
- pattern: yaml.load($DATA)
- pattern-not: yaml.load($DATA, Loader=yaml.SafeLoader)
- id: python-subprocess-shell-true
languages: [python]
severity: ERROR
message: shell=True turns any concatenated input into command injection.
pattern: subprocess.$F(..., shell=True, ...)
- id: python-tar-extract-no-filter
languages: [python]
severity: ERROR
message: tarfile.extractall without filter= allows path traversal.
patterns:
- pattern: $T.extractall(...)
- pattern-not: $T.extractall(..., filter=..., ...)二、漏洞就藏在规格里
编码代理是一个极其字面化的优化器。它拿到规格,写出满足规格的东西,然后停下。规格说「从用户给的路径加载 YAML 配置」,那么一个能在磁盘任意位置解析该路径的实现完全满足它。规格说「把上传的压缩包解到工作目录」,一个老老实实跟随 `../` 的解压也满足它。功能测试会通过,因为功能测试和规格共享同一个盲区。这就是为什么这些数字第一眼看上去矛盾、其实并不矛盾:代理很擅长它被交代的那份工作,而安全从来不在那份工作里。对一个沉默的规格,换更强的模型也问不出答案。
## Security contract — append this block to every agent spec
The specification above describes functionality only. These constraints are
part of the contract and are graded separately:
- Treat every byte that arrives from a file, socket, CLI argument or model
output as hostile. No exceptions for "internal" callers.
- Resolve paths against one declared root. Reject anything that escapes it
after normalisation, including symlinks and `..` segments.
- Deserialise with safe constructors only. Never instantiate classes named
in the input.
- Never build shell strings from input. Pass argument arrays.
- Secrets come from the environment. No defaults, no logs, no error text.
- Fail closed: a permission check that errors must deny.
If a requirement conflicts with a constraint, say so instead of choosing.三、把威胁模型从提示词搬进仓库
团队一直在用形容词修这个问题(「写安全代码」「遵循最佳实践」),而这些形容词熬不过一个长上下文。能熬过去的是仓库里的一个文件:它说明在这个项目里「恶意输入」意味着什么;再加上一套不管代理记不记得都会跑的机器可检规则。把安全契约写在规格旁边,把同一批规则写进 linter,让违规变成一行红色报错,而不是一条评审意见。下面三条是代理写的 Python 里重复率最高的:不安全的 YAML 加载、拼接 shell 字符串、不带 filter 的解压。
#!/usr/bin/env bash
# .ci/secure-gate.sh — the whole point is that both halves run before merge.
set -euo pipefail
# 1. Functionality the agent was asked for
pytest -q tests/unit tests/integration
# 2. Security the agent was never asked for
semgrep --config semgrep-rules/ --error --quiet .
# 3. Dependencies the agent quietly added
pip-audit -r requirements.txt --strict
# 4. Secrets the agent inlined "just for now"
gitleaks detect --no-banner --redact
# Anything that only passes step 1 is a demo, not a merge.四、把功能门禁和安全门禁放进同一个 Job
我见过最常见的流水线形态是:功能测试是合并的必需项,安全扫描每天夜里在 main 上跑,没人看。这个顺序等于保证了代理的产出只被它被告知的那半份契约评判。把扫描挪进合并门禁,并且让它把构建打红,而不是在 diff 上标个注释。在小型服务上这多花两分钟。别扭的不是工具,是那个决定:一条静态规则失败可以阻止合并。正是这个决定,决定了 25% 会不会滑向 16%。
from dataclasses import dataclass
from typing import Callable
@dataclass
class Case:
name: str # from a real past incident, not a CVE list
spec: str # the request you would actually type
functional: Callable # returns True when the feature works
exploit: Callable # returns True when the attack still works
def score(agent_fn, cases):
rows = []
for c in cases:
code = agent_fn(c.spec)
works = bool(c.functional(code))
unsafe = bool(c.exploit(code))
rows.append((c.name, works, unsafe))
n = len(rows)
return {
"cases": n,
"functional_rate": round(sum(w for _, w, _ in rows) / n, 3),
"safe_rate": round(sum(w and not s for w, s in [(w, s) for _, w, s in rows]) / n, 3),
"regressions": [r[0] for r in rows if r[1] and r[2]],
}
# Ten cases from your own incidents beat 450 you cannot reproduce.
# Run it on every harness change; the number moving is the signal.五、建一个十个用例的内部基准,每次改脚手架就跑一遍
你没法拿 LSCBench 去跑自己的代码,但你可以做一件更有用的事:把最近十次与安全相关的事故变成用例——只描述功能的规格、一个功能断言、一个「现在应该失败」的攻击。然后每次换代理、换模型、换脚手架、改提示模板、改工具白名单,都跑这套。你得到的分数不能和公开榜单比较,也不需要比较。它只回答一个问题:你刚改的东西有没有让你自己的代码更差。这是你的发布流程唯一能据以行动的问题。
REVIEW = {
"input_boundary": "Where does untrusted data enter, and what rejects it?",
"path_handling": "Can any argument, header or filename escape the root?",
"deserialization": "Which constructors run on attacker-controlled bytes?",
"shell": "Is there a string-built command anywhere in the diff?",
"authz": "Which check fails closed when the lookup raises?",
"secrets": "Did anything land in logs, errors or defaults?",
"deps": "What was added, and is it pinned and audited?",
}
def gate(diff_answers):
missing = [k for k, _ in REVIEW.items() if not (diff_answers.get(k) or "").strip()]
if missing:
raise SystemExit("review incomplete: " + ", ".join(missing))
return True六、这周我会改哪四件事
按顺序四件。第一,写出安全契约,粘进团队真正在用的规格模板里,不是粘进 wiki。第二,加上那三条 lint 规则,并让它们阻断合并。第三,拆分 CI 脚本:任何代理产出的 diff,都要先跑功能测试、静态规则、依赖审计和密钥扫描,再给人看。第四,挑上季度的一次事故,把它变成一条代理必须通过的测试。这些都不需要新模型,也都不激动人心。这正是重点:基准里的落差不是靠更好的提示词补上的,是靠把没写下来的要求写下来并检查补上的。
电路板上的安全锁,代表生成代码的安全风险
测试与静态检查通过时的终端输出
📌 常见问题 FAQ
Last Secure Code Benchmark 是什么?
一个评估编码代理安全代码生成能力的基准,以 arXiv 2610.14766 发布,2026 年 10 月 10 日提交。它包含由 150 个真实漏洞构造的 450 个任务,覆盖七种语言与 24 个 CWE 类别,其中包括 2025 年 CWE Top 25 的 21 个、Top 10 KEV 弱点中的 8 个。
核心结果是什么?
六个前沿代理的功能通过率为 73.6% 到 91.3%。最强的 Claude Fable 5 只有 25.1% 的产出同时做到功能正确与安全,其余五个在 16.4% 到 24.7% 之间。在三种生成设定下的漏洞场景里,最好的模型通过率为 12%。
为什么功能分远高于安全分?
因为每个任务的规格只写功能要求,从不提漏洞。功能测试与规格共享同一套假设,所以一个忽略路径穿越或不安全反序列化的实现照样能过。
LSCBench 的三种生成设定是什么?
填一个函数、写一个完整文件、从零搭出仓库源码树。平均生成长度从约 155 行升到 1,782 行,再升到 4,120 行。
那我是不是应该停用编码代理?
不是。它的意思是:安全要求必须被写下来并被机器检查,因为它不在你敲进去的那份规格里。仓库里的安全契约、能阻断合并的 lint 规则、与功能测试同一个 CI Job 的安全门禁,这三点就能补上落差,而不必放弃速度。