mcpField
O
owasp-audit

owasp-audit

github.com/briiirussell安全478611 次安装1 天前更新
代码仓库

关于 owasp-audit

owasp-audit 是按 OWASP Top 10:2025 系统审查源代码安全性的技能,面向获授权的代码审计。它区分咨询、完整审计和变更范围审查,并独立验证候选问题、记录覆盖情况。

主要功能

  1. 01OWASP Top 10:2025 审计流程
  2. 02按攻击面和信任边界记录覆盖
  3. 03独立验证候选问题
  4. 04验证 JSON 问题和覆盖数据
  5. 05根据审计记录生成报告

适用场景

  1. 01审查获授权代码库的安全问题
  2. 02合并前审计 PR 或提交范围
  3. 03在重新运行时跟踪覆盖和判定

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/briiirussell/cybersecurity-skills --skill owasp-audit

安装后在 Claude Code 中输入/owasp-audit确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
checklist-details.md
coverage.schema.json
diff-review.md
findings-format.md
findings.schema.json
orchestration.md
recon-and-coverage.md
severity-calibration.md
to-sarif.mjs
validate-coverage.mjs
validate-findings.mjs
verification.md
webhooks-and-parsers.md

详细介绍

原文在翻译后有更新,译文可能不是最新。

OWASP 审计 — 源代码安全审查

根据 OWASP Top 10(2021)对应用程序源代码进行系统性安全审计。

界定审计范围

  1. 确定项目使用的语言、框架和架构
  2. 梳理入口点(路由、API 处理程序、表单处理程序)
  3. 确定数据流(用户输入 → 处理 → 存储 → 输出)
  4. 找出身份验证和授权边界

审计清单

系统地检查每个类别。先 grep 查找已知漏洞模式,再阅读标记出的文件进行深入分析。

A01:访问控制失效
  • 端点或路由缺少授权检查
  • IDOR — 用户可控制的 ID 未经所有权验证
  • 授权检查顺序。 确认授权检查在任何可能泄露资源是否存在、当前状态或其他特定于资源的元数据的分支之前执行。对“不存在”返回 404、对“状态不正确”返回 400、对“未通过身份验证”返回 401,本身就会造成信息泄露——攻击者可以枚举资源 ID 并了解其状态,而无需通过授权检查。建议的响应形式:对于无权访问者不应查看的所有内容,统一返回 404。
  • 未以路由形式出现的框架 RPC 接口。 服务端操作及其同类机制是公开的 RPC,扫描文件时可能遗漏。逐一枚举并审计,确认每个接口都进行了身份验证和所有权检查:
    • Next.js:带有 'use server' 的文件中的每个导出函数
    • Remix / React Router:每个导出的 action / loader
    • tRPC:每个过程
    • GraphQL:每个解析器
    • Rails:非资源控制器操作
  • 通过变更载荷中的外键造成 IDOR。 表单提交外键 UUID(categoryId、projectId、teamId、organizationId)→ 服务端验证了主记录的所有权,却盲目接受外键 → ORM 关系联接随后暴露其他租户的数据。查找直接传入 insert / update 的 formData.get("<id>") / body.<id>,并确认之前是否进行了 findFirst({ where: { id, userId } })。对于 ORM 关系联接(Drizzle with:、Prisma include、ActiveRecord includes),追踪联接目标是否与父查询采用相同的租户/所有权筛选条件。
  • 状态变更请求缺少 CSRF 防护
  • 角色检查仅在前端进行,未在服务端强制执行
  • 身份验证后的返回地址参数导致开放重定向 — 将未经清理的 ?from=、?next=、?returnTo=、?continue=、?redirect= 传给 redirect() / Response.redirect()。限制为预期范围内的同源路径,并进行规范化(new URL(target, "http://localhost").pathname),以阻止 /admin/../foo 之类的路径穿越。同时,在重定向前拒绝路径中的控制字节:制表符/换行符/空字符(\t、\n、\0)——URL 解析器会移除这些字符,并将 /\tevil 合并为协议相对 URL //evil;空字符可能导致重定向返回 500。拒绝 [\x00-\x1F\x7F] 中的任何字节、任何反斜杠,以及任何百分号编码的斜杠/反斜杠(%2f、%5c)。
  • 查找:直接对象引用、缺失的身份验证中间件、来自请求参数的用户 ID、redirect(.*from、redirect(.*next、redirect(.*returnTo
A02:加密失败
  • 在源代码中硬编码的密钥、API 密钥或密码

  • 弱哈希(密码使用 MD5、SHA1,而不是 bcrypt/argon2/scrypt)

  • 对于 bcrypt,还要检查成本因子。OWASP 2024 指南建议 ≥ 12(成本因子为 10 时 ≈ 10ms / 每个攻击者核心每秒 100 次哈希)

  • 加密验证路径中的类型强制转换。 数值解析(parseInt、Number、parseFloat)会在遇到无效输入时静默返回 NaN,而 NaN 与 < 和 > 比较时结果都为 false。时间戳新鲜度检查 if (Math.abs(now - parsed) > tolerance) return false 无法拒绝 NaN——因为 NaN > tolerance 的结果为 false。搜索以下内容:verifySignature / validateToken / 签名 Cookie / JWT 声明代码中的 parseInt|parseFloat|Number\(.*\)。每次数值提取后,都必须在任何不等式判断之前执行 if (!Number.isFinite(parsed)) return false。同类问题:parseInt('0x123', 10) === 0、parseInt('1e10', 10) === 1、parseFloat('Infinity') === Infinity。

  • 日志、URL 或 localStorage 中的敏感数据

  • 缺少静态或传输中加密

  • **在建议 TLS 连接使用 VERIFY_PEER 之前,**先确定部署目标上的证书颁发者。许多托管服务在较低套餐中使用自签名证书链(Heroku Redis Mini/Hobby、某些 ElastiCache 配置、Supabase legacy)——如果未显式指定 ca_file:,VERIFY_PEER 就会失败。当确实无法使用 VERIFY_PEER 时,按优先顺序提供以下三种修复方案:

    1. 升级套餐或固定 CA 证书包——恢复证书验证
    2. 明确接受风险——保留 VERIFY_NONE,并满足以下条件:(a) 在每个调用点添加行内注释,(b) 记录补偿性控制措施(专用网络、仅限内部路由),(c) 创建后续问题,跟踪重新验证的条件
    3. 限制网络路径——专用子网 / VPC 对等连接 / 不公开到公网

    切勿在未检查部署目标上的证书链是否可验证时,悄然建议使用 VERIFY_PEER。

  • 搜索通用密钥名称和已知服务商密钥前缀:

    • 通用:password、secret、api_key、private_key、MD5、SHA1、base64
    • Stripe:sk_live_、sk_test_、rk_live_、whsec_
    • GitHub:ghp_、gho_、ghu_、ghs_、ghr_
    • AWS:AKIA[0-9A-Z]{16}、ASIA[0-9A-Z]{16}
    • Google Cloud:AIza[0-9A-Za-z\-_]{35}、服务账号 JSON("type": "service_account")
    • Slack:xox[baprs]-、xoxe.xoxp-
    • OpenAI / Anthropic:sk-、sk-ant-
    • Vercel:vercel_blob_rw_
    • 使用 git ls-files | xargs grep -lE 'sk_live|ghp_|AKIA[0-9A-Z]{16}|sk-ant-' 2>/dev/null 运行,以避免二进制文件和 Git 忽略的文件污染输出。
  • 扫描时要包括非源代码文件扩展名。 Rails 的 cable.yml / database.yml / storage.yml、Kubernetes 清单以及 Vercel / Netlify 部署配置中经常包含 TLS 或证书配置,仅扫描源代码会漏掉这些内容。针对 VERIFY_NONE / VERIFY_PEER 的具体扫描:

    grep -rn "VERIFY_NONE\|verify_mode" \
      --include="*.rb" --include="*.yml" --include="*.yaml" \
      --include="*.toml" --include="*.json" \
      .
    
A03:注入
  • SQL 注入: 使用字符串拼接的原始查询,缺少参数化查询

  • NoSQL 注入: MongoDB/Convex 查询中未经清理的用户输入

  • 命令注入: 使用用户输入调用 exec()、spawn()、system()

  • XSS: HTML 中未经转义的用户输入、dangerouslySetInnerHTML、v-html。

  • 通过 JSON.stringify 突破内联脚本。 任何通过 JSON.stringify 插入服务器数据的 <script type="application/ld+json"> 或 <script>window.__DATA__ = ...</script> 都存在漏洞——JSON.stringify 不会转义 <、>、&、U+2028 或 U+2029。包含 </script><script>alert(1)</script> 的存储标题会导致脚本突破。只有在保证每个字段都绝不会来自用户可编辑输入时,“仅限内部对象”的说法才有用。

    • 搜索:application/ld+json、__html: JSON.stringify、window.__ + JSON.stringify
    • 修复:在插入前使用转义辅助函数,将 <>&\u2028\u2029 替换为对应的 \uXXXX Unicode 转义序列。
  • Rails ERB 输出点: 对用户输入使用 raw()、.html_safe、<%==、带宽松允许列表的 sanitize 和 simple_format。搜索这些用法以及 dangerouslySetInnerHTML / v-html。

  • 清理器的选择。 修复 HTML/SVG XSS 输出点时,必须使用经过审查、基于解析器的清理器(JS 使用 DOMPurify / isomorphic-dompurify / sanitize-html;Python 使用 bleach)。代码审查中应拒绝基于正则表达式的清理器。如果无法避免使用,基于正则表达式的清理器必须:

    • 将 [/\s](而不只是 \s)视为属性名分隔符——HTML 允许在标签名和第一个属性之间使用 /:<img/onerror=…>
    • 同时剥除 SVG 和 HTML 命名空间中的危险元素(<img>、<body>、<video>、<iframe>)——即使在 SVG 渲染上下文中,HTML 元素也会被实例化
    • 添加最终兜底处理,无论上下文如何都剥除任何 on*= 属性
    • 配合 Content-Security-Policy: script-src-attr 'none',作为浏览器层的兜底防护
  • 将 SVG 上传文件作为存储型 XSS。 SVG 文件可以包含 <script> / onload。大多数 Blob / 对象存储会按照声明的内容类型提供上传文件。除非配置了清理器(例如 DOMPurify SVG 配置)并以 Content-Disposition: attachment 提供文件,否则应在上传允许列表中拒绝 image/svg+xml。

  • 写入和渲染时都要清理。 对于存储型 XSS / 注入,应在信任边界处(写入数据库)和渲染边界处(纵深防御)进行清理。发现存储型 XSS 漏洞时,应计划一次性回填迁移来清理现有数据——仅修复渲染会留下已污染的行,任何新的渲染路径都可能再次暴露这些数据。

  • Rails JSON-LD 突破。 在 <script type="application/ld+json"> 内,不要对字段值使用 j / escape_javascript——j 会生成 \' 和 \$(对 JS 有效,但对 JSON 无效),因此任何包含撇号或 $ 的字段都会导致 JSON.parse 失败。请改用以下写法:

    <% schema = { "@context" => "https://schema.org",
                  "@type" => "Article",
                  "headline" => @post.title } %>
    <script type="application/ld+json">
    <%= json_escape(schema.to_json).html_safe %>
    </script>
    

    to_json 会处理 JSON 转义;json_escape 会转义 <>&\u2028\u2029,防止通过 </script> 突破。通过往返测试进行验证:JSON.parse(json_escape(article.to_json)) 应等于源哈希。

  • 模板注入: 在模板字面量中使用用户输入

  • 搜索:exec(、eval(、innerHTML、dangerouslySetInnerHTML、$where、原始 SQL 字符串

A04:不安全的设计
  • 存在逻辑缺陷的身份验证流程

  • 敏感端点(登录、密码重置、API)缺少速率限制

  • 业务逻辑约束仅在客户端强制执行

  • 后台 / 即发即弃任务会继承调用方的身份验证上下文,但会丢失请求范围的防护措施。应在任务内部重新检查授权,而不只是入队时检查。搜索:Promise.all(...).catch(、void someAsync(、.catch(noop)、队列中的 enqueue(,但工作进程中没有重新验证授权。

  • 同类路由审计。 当你在某个处理程序中发现状态机或不可变性保护(例如 PUT /api/foo/[id] 上的 WHERE … AND signedAt IS NULL)时,搜索所有其他会写入同一张表的处理程序:

    rg 'update\(\s*tableName\b|\.update\(tableName' --type ts -B1 -A8
    

    每个调用点都需要相同的保护条件、相同的 userId 谓词,以及相同的冲突处理方式(returning() + 检查返回行数是否为 0)。常见问题:某个 POST /:id/send 或 POST /:id/convert 路由在 PUT 路由加固后上线,却从未重新审计。

  • 具有计费影响的外部资源创建 TOCTOU。 任何执行“先 SELECT 检查,再调用 provider.create(),然后 INSERT 记录新资源 ID”的处理程序,在并发情况下都可能在服务提供商一侧创建孤立资源。Stripe 账户、Auth0 / Clerk 用户、SendGrid 模板、S3 存储桶——无论是否存储了 ID,都会计费或占用配额。修复模式:

    1. 先抢占:使用 INSERT … ON CONFLICT DO NOTHING(数据库 UNIQUE 约束充当锁)
    2. 调用服务提供商
    3. 使用乐观保护条件持久化:UPDATE … SET externalId = ? WHERE externalId IS NULL,并检查返回行数是否为 0
    4. 如果竞争失败,尽力通过 provider.delete(id) 清理孤立资源;清理失败时记录日志
  • 工作队列状态转换需要原子抢占。 任何轮询待处理行的 cron / 工作进程,都必须在处理前原子地抢占每一行。SELECT + process() + UPDATE 存在竞态——两个工作进程(或两个重叠的 cron 调用)都会看到同一待处理行,并且都会发起调用,导致重复交付。修复方法:UPDATE … SET status='processing' WHERE id=? AND status='pending' RETURNING …——Postgres 的 RETURNING 让你在一次往返中完成抢占并读取。如果 UPDATE 返回 0 行,说明其他进程已经抢占了该行。对于吞吐量更高的队列,也可以使用 SELECT … FOR UPDATE SKIP LOCKED(Postgres / Cockroach)。

  • 多租户 Webhook 签名匹配。 未经身份验证的 Webhook 端点如果通过依次尝试每个租户的密钥来识别租户,那么每个请求——包括无效请求——都会执行 O(N) 次数据库查询和 O(N) 次 HMAC 计算。攻击者可以用随机签名发起洪泛,在永远无法通过身份验证的情况下放大 CPU / 数据库负载。防护措施(组合使用):

    1. 在访问数据库前进行签名格式预筛选——拒绝长度或字符集不符合服务提供商发送格式的签名(例如,HMAC-SHA256 十六进制签名使用 /^[a-f0-9]{64}$/i)
    2. 限制每个请求的签名检查次数(例如,LIMIT 200)
    3. 对该端点实施按 IP 限速
    4. 如果服务提供商支持,将租户 ID 嵌入 Webhook URL(/api/webhooks/foo/<connection_id>),让查询复杂度降至 O(1)
  • 限速键回退。 如果你的限速键包含攻击者可控或可能缺失的标识符(IP、用户 ID、会话 ID),那么在标识符缺失时,不要回退到所有请求共享的常量字符串。你可以选择:(a) 拒绝请求;(b) 回退到攻击者无法与他人共用的资源级标识符(注册时按电子邮件地址、计费时按 Stripe 客户);或 (c) 明确采用失败时放行并记录日志。共享的 'unknown' / 'anon' 桶可能导致拒绝服务——攻击者占满该桶,就能锁定所有通过该代理路径的用户。

  • 检查已配置但未加载的情况。 在断言某个安全中间件(限速、身份验证、CSRF、节流)“已配置”之前,请确认对应 gem / 软件包确实已安装,而不只是初始化文件存在。如果软件包没有被打包,if defined? Foo / if PACKAGE in sys.modules 包裹的初始化代码会静默地不执行。

    技术栈检查方式
    Ruby/Railsgrep -E "^ GEM_NAME " Gemfile.lock(4 个空格缩进 = 顶层 gem)
    Nodegrep "\"PACKAGE_NAME\":" package-lock.json 或 node -e "require('PACKAGE_NAME')"
    Pythonpip show PACKAGE_NAME
    Gogrep PACKAGE_PATH go.sum

    对于 Rails:还要确认中间件已加入运行时堆栈——bundle exec rails middleware | grep -i FOO。

A05:安全配置错误
  • 在生产配置中启用调试模式

  • CORS 策略过于宽松(Access-Control-Allow-Origin: *)

  • 缺少 HTTP 安全标头(CSP、HSTS、X-Frame-Options、X-Content-Type-Options)

  • 随附默认凭据或配置

  • 详细错误消息暴露堆栈跟踪或内部信息——包括验证库将架构详情(例如 Zod err.issues、Joi 错误树)回显给客户端

  • 安全标头基线起始值(粘贴并调整):

    标头值
    Strict-Transport-Securitymax-age=63072000; includeSubDomains; preload — 启用 preload 前,必须确认每个 *.example.com 都提供 HTTPS 服务
    X-Content-Type-Optionsnosniff
    X-Frame-OptionsDENY(与 CSP frame-ancestors 配合,提供纵深防御)
    Referrer-Policystrict-origin-when-cross-origin
    Permissions-Policycamera=(), microphone=(), geolocation=(), browsing-topics=() — 注意,interest-cohort 是旧版 FLoC 名称(Chrome ≤ 100);当前 Chrome 使用 browsing-topics
    Content-Security-Policy从 frame-ancestors 'self' 开始;完整的 CSP 需要逐站点审查脚本(内联 <style>、JSON-LD、分析代码)

    HSTS 预加载提交后很难撤回——移除需要数月。提交前请先确认。

  • 各框架中安全标头的配置位置:

    • Next.js:next.config.{js,ts} 中的 headers() 块;vercel.json 中的 headers
    • Rails:config/initializers/secure_headers.rb、config/application.rb
    • Express:app.use(helmet())
    • Django:settings.py 中的 SECURE_* 设置
  • 运行时与 API 不匹配。 在 Edge / Workers / V8 隔离运行时中运行的代码无法加载仅适用于 Node 的模块。在 Next.js middleware.ts / Cloudflare Workers / Vercel Edge 函数中导入 node:crypto、node:fs、node:buffer、node:net 等模块时,编译会成功,但首次请求时会因 Failed to load external module 而失败。检查中间件和标记为 edge 的路由中是否导入了仅适用于 Node 的模块;可移植代码优先使用 Web Crypto(crypto.subtle)

  • Rails 管理后台引擎挂载。 在 config/routes.rb 中搜索引擎和仪表板挂载(PgHero、Sidekiq::Web、Flipper UI、Mission Control、Audit1984),并确认对应初始化器中的身份验证中间件适用于互联网可访问的所有环境,而不只是生产环境:

    grep -E "mount .+::(Engine|Web|UI)|mount .+::App" config/routes.rb
    grep -rn "Rails.env.production?" config/initializers/ | \
      grep -B1 -A5 "Auth::Basic\|authenticate\|secure_compare"
    

    PgHero、Sidekiq::Web、Flipper 和 Mission Control 的 README 示例都将身份验证包装在 if Rails.env.production? 中,这会导致暂存环境、审查应用和预览部署匿名开放管理界面。修复方式:将条件改为 unless Rails.env.local?(Rails 7.1+ 的辅助方法,涵盖 development || test),并添加默认拒绝检查,在身份验证环境变量未设置时拒绝访问。

  • 付费端点上的并发执行竞态。 当端点会触发付费的下游工作(大模型调用、第三方 API、抓取任务)时,应检查是否存在先执行 SELECT,随后调用 runX(),但中间没有原子认领操作的情况,以及无条件执行 UPDATE … SET status='processing' 的写入。两个并发请求可能都通过读取检查并同时运行。修复方式:使用条件更新 UPDATE … WHERE id = ? AND status = 'pending' RETURNING …,或使用 Postgres advisory lock。仅在成功认领后扣除速率限制额度,避免轮询和重试消耗配额。

(注意:此条列在 A05 下,因为其故障表现为不正确配置的不变量保护机制。具体的竞态模式根据分析角度也可能属于 A04 / A05 / A08。)

  • 源代码树卫生。 搜索同步冲突产生的重复文件和无用代码,以防审查者修复了规范文件,却留下仍有漏洞的副本:

    find . \( -name '* 2.*' -o -name '* 3.*' -o -name '*.orig' \
         -o -name '*~' -o -name '*.bak' \) -not -path '*/node_modules/*'
    

    将相关发现归为 A05 — 规范文件可能已修补,但重复文件仍保留漏洞。

  • Next.js headers() 规则合并。 next.config.ts 中的 headers() 规则按路由匹配并进行合并——更具体的规则不会覆盖它未重新声明的标头。在 /:path* 默认规则中设置 frame-ancestors 'none' + X-Frame-Options: DENY,并在 /embed 覆盖规则中设置 frame-ancestors *,会导致 embed 路由同时返回 frame-ancestors * 和 X-Frame-Options: DENY(两者相互矛盾;较旧的浏览器可能无法正常显示框架内容)。使用 curl -I 检查已部署的源站——只检查配置无法发现合并结果。要么在每条规则中都设置 XFO,要么完全移除它(在现代浏览器中,CSP frame-ancestors 优先)。

  • 未排除 bearer/HMAC 路由的身份验证中间件。 Cron 任务、Stripe / GitHub webhook,以及任何通过 bearer 令牌或 HMAC 进行身份验证的路由,都需要从会话 Cookie 中间件中排除。表现:每次部署或定时调用时,这些路由都会悄无声息地 302 跳转到 /login,路由级签名检查从未执行,集成看似“正常运行”,直到出现问题。

  • 未设置环境变量插值的 bearer 令牌比较。 未检查变量是否存在就插入 process.env.X 的比较——\Bearer ${process.env.WEBHOOK_TOKEN}`——在环境变量缺失时会解析为字面字符串 "Bearer undefined"。攻击者若猜到环境变量未设置的情况,就能重放这个字面字符串。在模块加载时断言变量存在:if (!process.env.WEBHOOK_TOKEN) throw new Error(...)`。

  • API 路由返回 HTML 302 重定向,而非 JSON 401。 将每个未经身份验证的请求都 302 跳转到 /login 的身份验证中间件会破坏 fetch 客户端(它们会跟随重定向并接收 HTML),使监控中的身份验证状态变得不清晰,并让攻击者能通过 302 与 404 的区别判断端点是否存在。API 路由应返回 401 application/json,并提供机器可读的响应正文。

A06:存在漏洞的组件
  • 运行 npm audit(Node)、pip audit(Python)或同等工具
  • 检查锁文件中是否存在已知的易受攻击依赖版本
  • 标记存在严重 CVE 的依赖
  • 除了运行 npm audit,还要运行 npm audit --omit=dev,并根据可达性进行分类:
    • 运行时可达(位于 dependencies 中)——必须修复
    • 仅构建时使用(Vite、通过 drizzle-kit 引入的 esbuild、postcss)——通常可暂缓处理
    • 仅开发时使用(代码检查工具、测试库)——暂缓处理
  • 如需了解完整的 CVE 情况并按可达性分类,请调用 dependency-audit。此处的 A06 只是一个单行健全性检查。
A07:身份验证失败
  • 密码策略薄弱
  • 会话管理问题(缺少 secure/httpOnly 标志、无过期时间、无轮换)
  • 凭据即 Cookie: Cookie 值就是凭据(例如 cookies.set("admin_token", process.env.ADMIN_PASSWORD),读取时进行相等性检查)。即使设置了 httpOnly 和 secure,这仍是明文凭据存储(CWE-522),且缺少轮换和撤销机制。请改用通过 crypto.subtle.verify / crypto.timingSafeEqual 验证的 HMAC 签名限时令牌
  • 非恒定时间的凭据比较: 使用 submitted === expected 比较密码、API 密钥或签名,会通过计时泄露长度和前缀匹配情况。请使用 crypto.timingSafeEqual(Node)或 crypto.subtle.verify(Web Crypto)
  • 登录缺少速率限制(存在凭据填充攻击风险)
  • 密码重置流程存在缺陷
  • Rails / Devise — password_length 需要 :validatable。 只有当 User 模型在其 devise :... 声明中包含 :validatable 时,才会强制执行 config/initializers/devise.rb 中的 config.password_length。如果模型使用 devise :database_authenticatable, :registerable, :recoverable, :rememberable(没有 :validatable),则无论初始化文件中如何配置,任何长度的密码和任何格式的电子邮件地址都能通过验证。搜索:在 app/models/ 中查找 ^\s*devise\s+:;标记所有缺少 :validatable 的行。在现有应用中添加它会在创建和更新时执行验证,但不会追溯性地使现有弱密码失效。
  • NextAuth v5 / Auth.js 易踩问题:
    • 未设置 AUTH_SECRET 时,会静默派生一个较弱的开发环境值——请在模块加载时断言其存在:if (!process.env.AUTH_SECRET) throw ...
    • Credentials 提供程序的 authorize() 没有内置速率限制——请封装该函数或添加上游限流器;否则凭据填充攻击轻而易举
    • 通过注册错误提示(“email already exists”)枚举账户是否存在——请统一返回“请检查您的电子邮件”提示
    • JWT 策略 + DrizzleAdapter / PrismaAdapter——此组合下适配器几乎不起作用;必须显式设计撤销机制
  • 搜索:cookies.set\(.*process\.env、=== .*PASSWORD、!== .*SECRET、!== .*Bearer.*\${
A08:数据完整性失败
  • 不安全地反序列化用户输入
  • CI/CD 流水线缺少完整性检查
  • 未验证锁文件完整性(SRI 哈希)
  • 未检查状态就覆盖外部资源。 任何创建提供商侧资源(Stripe 支付意图、订阅、Webhook 订阅)并将其 ID 存入数据库行的处理程序,都应在覆盖前检查先前的资源是否仍在处理中。客户端可能仍持有旧的 clientSecret / 令牌;完成时,Webhook 处理程序可能会因 ID 已更改而找不到对应行(资金已到账,但数据库却永远停留在 processing 状态)。修复方法:创建新资源前,先检索现有资源。如果其状态未达到终态(processing、requires_payment_method、requires_action),且参数未变,则复用它——返回现有的 clientSecret。只有在先前资源已取消 / 成功,或参数发生变化时,才创建新资源。
  • 在数据库状态持久化前执行外部副作用。 如果处理程序既写入数据库,又触发外部副作用(电子邮件、扣款、Webhook、S3 写入),则外部调用应在数据库写入提交之后执行。“数据库状态已持久化,外部操作待重试”可以恢复;“外部操作已完成,数据库状态过时”则无法恢复。修复模式:先预留,再执行。发起条件 UPDATE,在前置状态的约束下将该行切换到操作后的状态(WHERE status='draft')。如果影响 0 行,则拒绝操作;如果影响 1 行,则调用提供商。如果提供商调用失败,该行已反映操作意图——发出告警并重试。额外好处:这也能让处理程序具备幂等性。
A09:日志记录与监控失效
  • 未记录身份验证事件(登录、失败、权限变更)
  • 日志中写入敏感数据(密码、令牌、个人身份信息)
  • 未对可疑模式发出警报
  • 静默吞掉错误。 空的 catch 块会掩盖程序错误和攻击信号(速率限制器失效、反序列化错误、身份验证失败)。
    • 搜索:catch \{\}、catch (_) \{\}、catch (_e) \{\}、.catch(() => {})、.catch(() => null)、try { ... } catch { return null }
    • 修复:至少记录错误类别(error.name、状态码),不包含个人身份信息
  • 未经身份验证的端点从已验证的域名向用户提供的地址发送电子邮件。 任何无需身份验证即可访问、并触发向攻击者提供的 to: 地址发送外发邮件的处理程序——例如确认邮件、待认领的密码重置邮件、邀请邮件、“我们已收到您的消息”邮件——尤其是在主题或正文受攻击者影响时,都是利用已验证发件人信誉进行钓鱼攻击的途径。防御措施(任选一项):
    1. 不自动确认——由人工回复时再发送回复
    2. 发送任何回复邮件前,要求证明所有权(验证链接 / 双重选择加入)
    3. 使用单独的、模板明确的 noreply@ 发件人,且主题和正文不受攻击者影响
A10:SSRF
  • 将用户控制的 URL 传递给服务器端 HTTP 请求
  • 缺少 URL 验证和允许列表
  • 只检查主机名的允许列表——真正的允许列表必须拒绝以下所有情况:
    • 错误的协议:http://allowed.com/(预期仅允许 https: 时)
    • 嵌入凭据:https://user:[email protected]/
    • @ 主机名技巧:https://[email protected]/(主机名解析为 evil.com)
    • 非默认端口:https://allowed.com:8443/
    • Punycode/IDN 欺骗:https://аllowed.com/(西里尔字母 а)或 xn--llowed-pdc.com
    • 末尾带点:https://allowed.com./(DNS 等效形式,字符串比较时经常漏掉)
    • 子域名混淆:https://allowed.com.evil.com/
    • 方括号括起的 IPv6 字面地址:https://[::1]/
    • 裸 IPv4 地址:https://127.0.0.1/
    • 十进制整数 IPv4 地址:http://2130706433/ → 127.0.0.1
    • 十六进制 IPv4 地址:http://0x7f000001/
    • 八进制 IPv4 地址:http://0177.0.0.1/;带前导零的 0010.0.0.1/ → 8.0.0.1(八进制!)
    • IPv4 映射的 IPv6 地址:http://[::ffff:127.0.0.1]/ → 屏蔽整个 ::ffff:* 范围
    • 主机名末尾带点:http://localhost./、http://metadata.google.internal./
    • 云元数据端点:AWS 169.254.169.254、GCP metadata.google.internal、ECS 169.254.170.2
    • CGNAT 范围:100.64.0.0 – 100.127.255.255
    • IPv6 链路本地地址:fe80::/10;IPv6 唯一本地地址:fc00::/7
  • 获取时的保护措施:设置 redirect: "error"(不跟随攻击者控制的重定向)、明确设置超时时间、不跟随 3xx 重定向到元数据服务
  • 注意验证与获取之间的 TOCTOU 问题——两者之间 DNS 解析结果可能不同(DNS 重绑定)。对于高风险调用方,固定解析出的 IP 并使用 Host: 标头按 IP 连接,或使用经过审核的代理
  • 将图像优化器用作代理(Next.js、Nuxt、SvelteKit):next.config.{ts,js} 中的 images.remotePatterns: [{ hostname: '**' }]、domains: ['*'] 或任何通配符条目,都会让攻击者通过你的 CPU/带宽路由任意 URL。
    • 搜索:图像配置中的 remotePatterns、domains:
    • 修复:指定已知主机名;如果所有图像都是本地的,则留空。
  • 搜索:包含用户输入的 fetch(、axios(、http.get(、urllib、requests.get(

在运行时验证修复

应用修复后,实际运行受影响的代码路径——不要止步于类型检查或构建。现代框架存在仅在运行时出现、但编译时不会报错的故障模式:

  • Edge / Node 分离运行时。 Next.js 中间件、Cloudflare Workers、Vercel Edge——仅限 Node 的导入(node:crypto、node:fs)可以成功构建,但会在首次请求时抛出异常。
  • 延迟加载模块。 通过 import() 或运行时 DI 加载的适配器/插件,只有在代码路径实际运行时才会暴露问题。
  • 环境变量回退。 X 未设置时,Bearer ${process.env.X} 会变成字面值;测试环境定义了 X,因此测试从未触发此问题。

对于每项已发布的修复,都要运行受影响的路由或作业,并捕获响应。tsc --noEmit + 构建成功 ≠ 已验证修复。

对于 XSS / 清理器配置修复,请将标准载荷集通过已配置的策略运行,并确认每个载荷都已被中和:

<img src=x onerror=alert(1)>
<a href="javascript:alert(1)">x</a>
<a href="data:text/html,<svg onload=alert(1)>">x</a>
<img srcset="javascript:alert(1) 1x,https://ok.com/a.png 2x">
<svg><script>alert(1)</script></svg>
<math><mtext></style><img src=x onerror=alert(1)></math>
<a href="//evil.com">protocol-relative</a>
<svg></svg><img/onerror=alert(1) src=x>

第二轮审查

单轮审计通常能可靠地发现检查清单上的问题类别,却会漏掉清单中未列出的特定绕过方式(localhost. 与 localhost 的差异、IPv4 映射的 IPv6、通过有序的 404/400/401 枚举状态、回调 URL 控制字符、付费端点上的并发执行竞态)。

生成第一份报告后,以及应用修复后,都要以明确的对抗性视角进行第二轮审查(“假设作者过度自信;找出他们遗漏的问题”)——最好由完全不同的模型或 Agent 执行,以打破相关盲区。对第一轮审查结果的任何异议,都应视为更有价值的发现。

第二轮审查中常见的发现包括:

  • 修复本身引入的新攻击面(通过豁免路由绕过身份验证、新查询引入 IDOR)
  • 重写过程中变得过时的注释
  • 新代码中的边界条件(环境变量未设置时的回退、空输入)
  • 修复内容与报告之间的文档不一致
  • 配置第三方库的修复。 当修复采用库自身文档中的代码片段(身份验证、加密、HTTP 客户端、速率限制中间件)时,代码片段可能正确,却仍未在你的代码路径中运行。在宣布问题已修复前:对照锁定的版本 grep 该库,并从你的调用点追踪到配置实际影响的代码路径。示例:如果你以编程方式调用 auth.api.signInEmail(...),启用 Better Auth 的 rateLimit 并无帮助——因为这会绕过限流器所挂接的 HTTP 路由器。

报告格式

对于每个 OWASP 类别,记录以下三种状态之一:

  • 发现的问题(附严重程度 + 修复措施)
  • 无问题——明确写明“已检查 X,未发现问题”,并说明你搜索了哪些内容
  • 不适用——解释该类别为何不适用(例如:“A07 不适用:此代码库中没有身份验证入口”)

在执行摘要中加入“已检查且确认无问题的项目”部分。审计的可信度来自证明你确实检查过,而不只是列出发现的问题。

每项发现有三种可能的处置方式:

  • 已修复——已在此 PR 中发布修复。关闭,等待验证。
  • 已延期——已确认并安排修复。说明下次部署是否以此为门槛(发布阻塞项),还是不受影响(可接受风险,已有按日历安排的修复)。决定延期不会改变严重程度。
  • 接受的风险——当前配置下不计划修复。报告必须记录:
    1. 修复不适用的原因——成本层级、依赖版本限制、部署拓扑、供应商限制
    2. 补偿性控制措施——私有网络、签名 Cookie、仅限内部的路由等
    3. 重新评估触发条件——哪些条件(升级套餐、更新依赖、流量模式变化)会使此问题不再属于“接受的风险”

缺少以上任意一项的“接受的风险”条目,实质上就是将真实问题悄然改用另一个标签搁置。

对于每项发现,记录:

#### [SEVERITY] A0X: [Title]
**File:** `path/to/file.ts:42`
**CWE:** CWE-XXX

**Description:** [What the vulnerability is and why it matters]

**Vulnerable Code:**
[code snippet]

**Remediation:**
[Fixed code snippet with explanation]

**Verification:** Concrete adversarial input + the command or code path that proves the fix holds. For XSS: the script-tag breakout payload that no longer breaks out. For open redirects: the off-host URL that now rejects. For password-length: the 1-char password that now fails to save. "The linter says it's fine" is not verification — static analysis has known blind spots for correctness bugs that happen to also be security fixes.

生成一份执行摘要:

# Security Audit Report
## Project: [name]
## Stack: [technologies]
## Date: [date]

### Summary
- Total findings: X
- Critical: X | High: X | Medium: X | Low: X | Info: X

### Findings
[Individual findings as above]

### Prioritized Remediation Plan
1. [Critical fixes — immediate]
2. [High fixes — this week]
3. [Medium/Low — scheduled]

范围

  • 仅审计用户提供或指定的代码
  • 提供修复方案,而非利用方法;始终包含补救措施
  • 将置信度较低的发现标记为“潜在”,而非已确认
  • 如果代码库过大,无法全面审计,请优先审查:身份验证、输入处理、数据访问层
  • 拒绝插入后门或削弱安全控制的请求

参考资料

  • OWASP Top 10 (2021)
  • OWASP Code Review Guide
  • CWE Top 25

评论

支持 Markdown