关于 reviewing-security
面向开发者的安全审查技能,强调追踪攻击者可控输入到实际影响的代码路径,而非只列漏洞类别。涵盖攻击面、授权、数据流、依赖项及基于证据的报告。
主要功能
- 01追踪攻击输入至实际影响
- 02梳理入口点与运行权限
- 03检查授权与数据暴露
- 04验证扫描结果是否可达
- 05按可利用性评级并报告
适用场景
- 01审查代码库中的可利用漏洞
- 02检查对象与租户访问授权
- 03报告前验证扫描器告警
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ osmontero/ opencode-skills --skill reviewing-security安装后在 Claude Code 中输入/reviewing-security确认已加载。
附带文件references/vulnerability-patterns.md
详细介绍
审查安全性
概述
大多数安全审查结果都毫无价值,因为它们只列出类别,而不是路径。“验证所有用户输入”适用于以往编写的每个程序,毫无作用。有用的发现是一条路径:攻击者控制的输入、传递该输入的代码、它到达的操作,以及攻击者能获得什么。
**核心原则:**追踪数据,而不是照着清单逐项检查。漏洞存在于值进入系统的位置与它发挥作用的位置之间。
**配套技能:**通用审查流程请参阅 requesting-code-review;确认攻击路径时请参阅 systematic-debugging。
铁律
NO FINDING WITHOUT A PATH FROM ATTACKER-CONTROLLED INPUT TO IMPACT
如果你说不清输入是什么、它经过什么路径,以及它会给操作人员造成什么代价,那这就是代码异味——应将其归入“观察项”,而不是“发现项”。用类别性建议充斥报告,会掩盖那条确实存在的路径。
**推论:**报告问题前,先在实际代码中追踪路径。grep 命中只是线索,不是发现项。
检查清单
- 梳理攻击面——入口点、哪些人能访问、它们以什么权限运行
- 追踪输入到接收点——各种形式的注入
- 授权——收益最高的检查环节
- 密钥与加密
- 进程与插件边界
- 依赖项
- 数据暴露——日志、错误、响应
- 运行扫描器——然后确认可达性,再报告
- 报告——按可利用性评定严重程度,每项都要附上路径
第 1 步:梳理攻击面
在阅读实现代码前先完成这一步。你要列出陌生人能够触及的位置。
| 入口点 | 谁能访问 | 运行时使用的权限 |
|---|---|---|
| HTTP 路由、gRPC 方法 | 匿名用户 / 已认证用户 / 管理员 / 仅限内部 | 进程、数据库角色、文件系统 |
| 队列或事件使用者 | 能发布消息的任何人 | 通常拥有完整进程权限 |
| 文件上传、导入 | 任何已认证用户 | 解析器在进程内运行 |
| Webhook 接收器 | 互联网用户,直到签名得到验证 | |
| CLI 标志、环境变量、配置 | 本地操作人员 | |
| LLM / 工具输出 | 任何能影响模型上下文的人 | 工具能执行的任何操作 |
# Enumerate handlers and consumers — adapt the pattern to the framework
grep -rnE '\.(Get|Post|Put|Patch|Delete|Handle|HandleFunc)\(' --include='*.go' --include='*.ts'
grep -rnE 'func \(.*\) [A-Z][A-Za-z]*\(ctx context.Context' --include='*.go' # gRPC impls
最后一行并不新奇。**模型输出是不可信输入。**如果文档、网页或先前的工具结果能引导模型调用某个工具,那么该工具能触及的每个接收点都可被攻击者触及。审查工具定义时,应像审查 HTTP 处理程序一样保持警惕。
步骤 2:从输入追踪到接收点
对于每个入口点,追踪值的流向。接收点是指任何行为会因该值具有恶意而发生变化的操作。
| 接收点 | 危险之处 | 安全形式 |
|---|---|---|
| SQL / 查询构建器 | 字符串拼接或插值 | 参数化查询;标识符无法参数化,需使用允许列表 |
| Shell / 进程启动 | Shell 解释器,或 argv[0] 中包含用户数据 | 使用 argv 数组执行,不调用 Shell,二进制文件路径固定 |
| 文件系统路径 | ..、绝对路径、符号链接、NUL | 拼接路径,然后调用 filepath.EvalSymlinks,再确认结果仍位于根目录下 |
| 出站 HTTP | 用户提供的 URL → SSRF | 对主机使用允许列表;阻止私有、链路本地和元数据地址段;重定向后重新检查 |
| 模板渲染 | 关闭自动转义,或使用 dangerouslySetInnerHTML | 默认转义;使用真正的清理器清理 HTML |
| 反序列化 | 多态或携带类型的格式 | 使用固定架构,解码为具体类型,并限制大小和深度 |
| 归档解压 | zip-slip、符号链接、解压炸弹 | 验证每个条目的路径,限制总字节数和条目数量 |
| 重定向目标 | 开放重定向 → 网络钓鱼、令牌窃取 | 对路径使用允许列表,或仅允许同源 |
| 对用户输入执行正则表达式 | 灾难性回溯 | 限制输入长度,避免嵌套量词 |
eval / 反射 / 动态导入 | 任意代码 | 没有安全形式——移除它 |
跨边界追踪值。 常见的疏漏是:在边界处验证值、将其存储起来,之后又读取并信任它。存储不等于清理。如果之后会渲染或执行该值,它仍然是不可信的。
每行对应的具体易受攻击 / 修复示例:参见 references/vulnerability-patterns.md。
步骤 3:授权
真正的漏洞就在这里。 现代库已能很好地防御注入;但对象级授权缺陷并非如此,因为框架无法修复这类问题——这需要知道哪些行属于谁。
- 每个接受 ID 的处理程序都会验证调用者是否有权访问 该 对象,而不只是验证其是否已登录
- 检查必须位于数据实际经过的路径上;路由层的检查若可被服务层绕过,就不算检查
- 租户 / 组织范围限制应写在查询中(
WHERE tenant_id = ?),而不是在获取结果后再应用 - 不信任请求正文中的角色、租户、价格或用户 ID——批量赋值只是多绕几步的授权绕过
- 默认拒绝:没有显式策略的新路由应无法访问,而非公开可用
- 会改变状态的操作不能通过
GET访问 - 顺序 ID 不能成为用户访问他人记录的唯一屏障
- 管理员界面的访问控制不能只依赖 URL 未公开
- 多步骤流程的每一步都要重新检查授权,而不只是在开始时检查
# Handlers that take an identifier — each one needs an ownership check
grep -rnE 'Param\("id"\)|params\.id|req\.body\.(userId|tenantId|role)' --include='*.go' --include='*.ts'
步骤 4:密钥与加密
- 源代码、提交到 git 的配置、日志、错误消息、URL 或客户端可见的资源包中均不包含密钥
- git 历史记录中也不包含密钥——轮换任何曾提交过的密钥;之后在提交中删除也于事无补
- 使用 argon2id/scrypt/bcrypt 对密码进行哈希——绝不能直接使用 SHA,也绝不能不加盐
- 使用恒定时间函数比较令牌和签名
- 令牌、ID 和盐的随机数必须来自 CSPRNG(
crypto/rand、crypto.randomUUID)——绝不能使用math/rand - JWT:验证签名,固定算法(拒绝
none和算法混淆),检查exp/aud/iss - 绝不禁用 TLS 验证——不使用
InsecureSkipVerify: true,也不设置NODE_TLS_REJECT_UNAUTHORIZED=0 - 权限变更时轮换会话令牌;注销时在服务器端使其失效
- Cookie:有意设置
HttpOnly、Secure、SameSite - 不手写加密原语或自定义“加密”方式
gitleaks detect --no-banner # or: trufflehog git file://.
git log -p -S'BEGIN PRIVATE KEY' --oneline | head
第 5 步:进程与插件边界
对于会生成子进程、加载插件或代表模型执行工具的任何操作:
- 子进程只继承所需的环境变量,而不是包含所有凭据的整个父进程环境
- 工作目录、打开的文件描述符和网络访问均经过明确设置,而非默认继承
- 子进程输出会被解析为数据,绝不会重新作为受信任的指令输入
- 存在资源限制:超时时间、内存和输出大小。没有限制的子进程是拒绝服务的手段
- 失败时采取默认拒绝策略——插件崩溃或无响应时,应拒绝操作,而不是跳过检查
- 工具本身就是安全边界:接受路径、URL 或命令的工具就是相应的危险操作点,会暴露给任何能影响模型的人
第 6 步:依赖项
- 已提交锁文件,并在构建中严格遵循(
npm ci、go mod verify) - 漏洞公告扫描无问题;或者每项例外都有书面说明,并论证其可达性
- 对新的直接依赖项,检查其发布时间、维护情况、下载量,以及名称是否可能是仿冒包
- 审查任何新依赖项的安装脚本/postinstall 脚本
- CI 操作固定到 SHA,而不是会变动的标签
govulncheck ./... # Go — reports only vulnerabilities you actually reach
npm audit --omit=dev # Node
pip-audit # Python
govulncheck 比通用扫描器更有价值,正是因为它会进行可达性分析。优先使用能告知你易受攻击的函数是否会被调用的工具。
第 7 步:数据暴露
- 返回给客户端的错误不包含堆栈跟踪、SQL、文件路径或内部主机名
- 日志不包含密码、令牌、会话 ID、完整卡号,也不包含超出必要范围的个人身份信息(PII)
- 生产环境中已禁用或限制访问调试端点(
/debug/pprof、GraphQL 内省、管理控制台) - CORS:不得同时使用
Access-Control-Allow-Origin: *和凭据;明确列出允许的来源 - 经过身份验证的响应不可由共享缓存缓存
- 对象存储桶及其列表默认设为私有
- 时间差异和错误消息差异不会泄露某个账户是否存在
第 8 步:运行扫描器,然后验证
semgrep --config=auto .
扫描器的输出是一份线索清单,而非报告。报告任何扫描器发现之前,请先确认它在此代码库中可达——检查危险操作点是否确实使用了受污染的值。直接转发工具的原始输出,只会让审查变成被忽视的噪音,真正的问题也会因此被埋没。
严重程度
根据可利用性评级,而不是根据类别名称。
| 严重程度 | 定义 |
|---|---|
| 严重 | 未经身份验证的攻击者可执行代码、获取凭据或批量访问其他用户的数据。无需满足特殊前提条件。 |
| 高 | 经过身份验证的攻击者可提升权限、读取或写入其他租户的数据,或窃取机密。 |
| 中 | 需要特殊前提条件(特定竞态、漏洞链或用户操作),或只能获取有限数据。 |
| 低 | 加固建议。存在真实弱点,但目前尚未证明可利用路径——缺少纵深防御层,可能无法遏制未来出现的漏洞。 |
不要夸大严重程度。如果一份报告中的所有问题都是严重级别,就不会有人采取行动。
报告格式
## Scope
[What was reviewed — diff range, paths, or "full repo". What was NOT reviewed.]
[Scanners run and their versions.]
## Critical
**Tenant isolation missing on document fetch**
`internal/api/documents.go:88` — the handler loads by primary key only:
`db.First(&doc, c.Param("id"))`. There is no `tenant_id` predicate and no
post-fetch ownership check.
**Path:** any authenticated user → `GET /api/documents/{id}` → sequential
integer IDs → full document body of any tenant.
**Impact:** cross-tenant read of all documents; IDs are enumerable.
**Fix:** scope the query — `db.Where("tenant_id = ?", claims.TenantID).First(&doc, id)`
— and add the predicate at the repository layer so new callers inherit it.
## High
[same shape]
## Medium / Low
[same shape]
## Observations (not findings)
[Weaknesses with no demonstrated path. Say so plainly rather than promoting them.]
## Verified Clean
[What you checked and found sound. Silence reads as "not checked".]
## Assessment
**Ship?** Yes / No / After the Critical and High items
**Reasoning:** [1-2 sentences]
不应报告的内容
| 不要报告 | 原因 |
|---|---|
| 没有危险操作点,却建议“添加输入验证” | 这是笼统建议,不是发现 |
| 没有指出可被滥用的端点,却指出缺少速率限制 | 请指出端点和代价,否则就删除 |
| 不提供浏览器客户端的服务缺少浏览器安全标头 | 该威胁不适用 |
| 测试、测试夹具或示例代码中的发现 | 攻击者无法触及 |
| 未确认可达性的扫描器输出 | 这种噪音会掩盖真正的问题 |
| 没有威胁背景的密码学偏好吹毛求疵 | “使用 AES-256,而不是 AES-128”不算发现 |
| 任何你会称为“理论上”的内容 | 如果确实可利用,你就应该能指出具体路径 |
常见错误
审查清单,而不是审查路径。 按顺序逐项检查 OWASP Top 10,会发现框架已经处理的漏洞,却会漏掉该领域特有的授权漏洞。
只检查入口点。 即使入口处做了验证,如果值被存储起来,之后又被拼接到查询中,这些验证也毫无意义。
盲目信任框架。 ORM 有原始查询的绕过接口,模板有原始输出的绕过接口,而且两者都会被使用。请专门搜索这些接口。
把已通过身份验证视为已获授权。 这是现实中最常见的发现,也最容易因为只看路由表、不看查询而被漏掉。
认为模型可信。 如果工具调用会受到模型读取内容的影响,其参数就由攻击者控制。
夸大严重性以显得全面。 这会让读者忽视真正重要的发现。