owasp-audit

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

OWASP Audit — Source Code Security Review

OWASP审计——源代码安全评审

Perform a systematic security audit of application source code against the OWASP Top 10 (2021).
针对OWASP Top 10(2021)对应用程序源代码进行系统性安全审计。

Scope the Audit

审计范围界定

  1. Identify the project's language, framework, and architecture
  2. Map entry points (routes, API handlers, form processors)
  3. Identify data flows (user input → processing → storage → output)
  4. Locate authentication and authorization boundaries
  1. 确定项目的语言、框架和架构
  2. 映射入口点(路由、API处理程序、表单处理器)
  3. 识别数据流(用户输入→处理→存储→输出)
  4. 定位身份验证和授权边界

Audit Checklist

审计检查清单

Work through each category systematically. For each, grep for known vulnerability patterns, then read flagged files for deeper analysis.
按每个类别系统性开展工作。对于每个类别,先通过grep查找已知漏洞模式,再阅读标记文件进行深度分析。

A01: Broken Access Control

A01: Broken Access Control(访问控制失效)

  • Missing authorization checks on endpoints or routes
  • IDOR — user-controlled IDs without ownership verification
  • Auth-check ordering. Verify the authorization check runs before any branch that can reveal whether the resource exists, what state it's in, or any other resource-specific metadata. Returning 404 for "not found", 400 for "wrong state", and 401 for "not authenticated" is itself a leak — an attacker enumerates resource IDs and learns states without ever passing the auth gate. Recommended response shape: uniform 404 for everything an unprivileged caller should not see.
  • Framework RPC surfaces that don't appear as routes. Server actions and equivalents are publicly-exposed RPCs that file scans miss. Enumerate and audit each one for auth + ownership:
    • Next.js: every exported function in a file with
      'use server'
    • Remix / React Router: every
      action
      /
      loader
      export
    • tRPC: every procedure
    • GraphQL: every resolver
    • Rails: non-resource controller actions
  • IDOR via foreign keys in mutation payloads. Form posts a foreign-key UUID (
    categoryId
    ,
    projectId
    ,
    teamId
    ,
    organizationId
    ) → server validates ownership of the primary record but blindly accepts the FK → ORM relation join later surfaces another tenant's data. Look for
    formData.get("<id>")
    /
    body.<id>
    passed straight to insert/update without a preceding
    findFirst({ where: { id, userId } })
    . For ORM relation joins (Drizzle
    with:
    , Prisma
    include
    , ActiveRecord
    includes
    ), trace whether the join target is filtered by the same tenant/ownership predicate as the parent query.
  • Missing CSRF protections on state-changing requests
  • Role checks only on the frontend, not enforced server-side
  • Open redirect via post-auth return-to parameter —
    ?from=
    ,
    ?next=
    ,
    ?returnTo=
    ,
    ?continue=
    ,
    ?redirect=
    passed unsanitized to
    redirect()
    /
    Response.redirect()
    . Restrict to same-origin paths under the expected scope, normalize (
    new URL(target, "http://localhost").pathname
    ) to defeat traversal like
    /admin/../foo
    . Also reject control bytes in the path before redirect: tab/newline/null (
    \t
    ,
    \n
    ,
    \0
    ) — URL parsers strip these and collapse
    /\tevil
    into protocol-relative
    //evil
    ; null bytes can turn the redirect into a 500. Reject any byte in
    [\x00-\x1F\x7F]
    , any backslash, and any percent-encoded slash/backslash (
    %2f
    ,
    %5c
    ).
  • Grep for: direct object references, missing auth middleware, user ID from request params,
    redirect(.*from
    ,
    redirect(.*next
    ,
    redirect(.*returnTo
  • 端点或路由缺少授权检查
  • IDOR——用户可控ID未进行所有权验证
  • 授权检查顺序:验证授权检查是否在任何可能泄露资源是否存在、状态或其他资源特定元数据的分支之前执行。对未授权调用者不应看到的内容统一返回404,而不是分别返回404(未找到)、400(状态错误)和401(未认证)——后者会让攻击者枚举资源ID并在未通过授权验证的情况下获取状态信息。推荐响应格式:对所有无权限调用者统一返回404。
  • 未以路由形式呈现的框架RPC接口:Server actions及等效功能是公开暴露的RPC,文件扫描可能会遗漏。需枚举并审计每个接口的授权与所有权验证:
    • Next.js:所有包含
      'use server'
      文件中的导出函数
    • Remix / React Router:所有
      action
      /
      loader
      导出函数
    • tRPC:所有过程(procedure)
    • GraphQL:所有解析器(resolver)
    • Rails:非资源控制器动作
  • 通过变更负载中的外键实现IDOR:表单提交外键UUID(
    categoryId
    projectId
    teamId
    organizationId
    )→服务器验证主记录的所有权但盲目接受外键→后续ORM关联查询暴露其他租户的数据。查找直接将
    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
    转换为协议相对路径
    //evil
    ;空字符可能导致重定向返回500错误。拒绝
    [\x00-\x1F\x7F]
    中的任何字节、任何反斜杠以及任何百分比编码的斜杠/反斜杠(
    %2f
    %5c
    )。
  • 搜索关键词:直接对象引用、缺少授权中间件、请求参数中的用户ID、
    redirect(.*from
    redirect(.*next
    redirect(.*returnTo

A02: Cryptographic Failures

A02: Cryptographic Failures(加密失败)

  • Hardcoded secrets, API keys, or passwords in source
  • Weak hashing (MD5, SHA1 for passwords instead of bcrypt/argon2/scrypt)
  • For bcrypt, also check the cost factor. OWASP 2024 guidance is ≥ 12 (cost 10 ≈ 10ms / 100 hashes/sec/core for an attacker)
  • Type coercion in cryptographic-verification paths. Numeric parsing (
    parseInt
    ,
    Number
    ,
    parseFloat
    ) silently produces
    NaN
    for garbage input, and
    NaN
    compares as
    false
    for both
    <
    and
    >
    . A timestamp-freshness check
    if (Math.abs(now - parsed) > tolerance) return false
    fails to reject
    NaN
    — because
    NaN > tolerance
    is
    false
    . Grep for:
    parseInt|parseFloat|Number\(.*\)
    inside
    verifySignature
    /
    validateToken
    / signed-cookie / JWT-claim code. Each numeric extraction must be followed by
    if (!Number.isFinite(parsed)) return false
    before any inequality. Same family:
    parseInt('0x123', 10) === 0
    ,
    parseInt('1e10', 10) === 1
    ,
    parseFloat('Infinity') === Infinity
    .
  • Sensitive data in logs, URLs, or localStorage
  • Missing encryption at rest or in transit
  • Before recommending
    VERIFY_PEER
    for a TLS connection,
    identify the cert issuer at the deployment target. Many managed services ship self-signed cert chains at lower tiers (Heroku Redis Mini/Hobby, some ElastiCache configurations, Supabase legacy) —
    VERIFY_PEER
    fails there without an explicit
    ca_file:
    pin. When
    VERIFY_PEER
    is genuinely infeasible, present three remediation options in priority order:
    1. Upgrade the plan or pin the CA bundle — restores cert verification
    2. Accept the risk explicitly — leave
      VERIFY_NONE
      with (a) an in-line comment at every call site, (b) a documented compensating control (private network, internal-only routing), (c) a follow-up issue tracking re-verification conditions
    3. Restrict the network path — private subnet / VPC peering / no public exposure
    Never quietly recommend
    VERIFY_PEER
    without checking that the cert chain at the deployment target is verifiable.
  • Grep for generic secret names AND known provider key prefixes:
    • Generic:
      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}
      , service-account JSON (
      "type": "service_account"
      )
    • Slack:
      xox[baprs]-
      ,
      xoxe.xoxp-
    • OpenAI / Anthropic:
      sk-
      ,
      sk-ant-
    • Vercel:
      vercel_blob_rw_
    • Run via
      git ls-files | xargs grep -lE 'sk_live|ghp_|AKIA[0-9A-Z]{16}|sk-ant-' 2>/dev/null
      so binaries and gitignored files don't pollute output.
  • Include non-source file extensions in the sweep. Rails
    cable.yml
    /
    database.yml
    /
    storage.yml
    , Kubernetes manifests, and Vercel / Netlify deploy configs routinely contain TLS or cert config that a source-only sweep misses. Concrete sweep for VERIFY_NONE / VERIFY_PEER:
    bash
    grep -rn "VERIFY_NONE\|verify_mode" \
      --include="*.rb" --include="*.yml" --include="*.yaml" \
      --include="*.toml" --include="*.json" \
      .
  • 源代码中硬编码密钥、API密钥或密码
  • 弱哈希算法(密码使用MD5、SHA1而非bcrypt/argon2/scrypt)
  • 对于bcrypt,还需检查成本因子。OWASP 2024指南要求≥12(成本因子10≈攻击者每核每秒可计算100次哈希,耗时10ms)
  • 加密验证路径中的类型转换问题:数值解析(
    parseInt
    Number
    parseFloat
    )会对无效输入静默生成
    NaN
    ,而
    NaN
    与任何值的
    <
    >
    比较结果均为
    false
    。时间戳新鲜度检查
    if (Math.abs(now - parsed) > tolerance) return false
    无法拒绝
    NaN
    ——因为
    NaN > tolerance
    false
    。搜索关键词:
    parseInt|parseFloat|Number\(.*\)
    出现在
    verifySignature
    /
    validateToken
    /签名Cookie/JWT声明代码中。每个数值提取后必须在进行任何比较前添加
    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旧版本)——若无显式
    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
    的具体命令:
    bash
    grep -rn "VERIFY_NONE\|verify_mode" \
      --include="*.rb" --include="*.yml" --include="*.yaml" \
      --include="*.toml" --include="*.json" \
      .

A03: Injection

A03: Injection(注入)

  • SQL injection: raw queries with string concatenation, missing parameterized queries
  • NoSQL injection: unsanitized user input in MongoDB/Convex queries
  • Command injection:
    exec()
    ,
    spawn()
    ,
    system()
    with user input
  • XSS: unescaped user input in HTML,
    dangerouslySetInnerHTML
    ,
    v-html
    .
  • Inline-script breakout via
    JSON.stringify
    .
    Any
    <script type="application/ld+json">
    or
    <script>window.__DATA__ = ...</script>
    that interpolates server data through
    JSON.stringify
    is vulnerable —
    JSON.stringify
    does NOT escape
    <
    ,
    >
    ,
    &
    , U+2028, or U+2029. A stored title containing
    </script><script>alert(1)</script>
    will break out. The "internal-only object" framing only saves you when every field is guaranteed never to come from user-editable input.
    • Grep for:
      application/ld+json
      ,
      __html: JSON.stringify
      ,
      window.__
      +
      JSON.stringify
    • Fix: wrap with an escape helper that replaces
      <>&\u2028\u2029
      with their
      \uXXXX
      Unicode escapes before injecting.
  • Rails ERB sinks:
    raw()
    ,
    .html_safe
    ,
    <%==
    ,
    sanitize
    with a permissive allowlist, and
    simple_format
    on user input. Grep for these alongside
    dangerouslySetInnerHTML
    /
    v-html
    .
  • Sanitizer choice. When remediating an HTML/SVG XSS sink, the fix MUST use a vetted parser-based sanitizer (DOMPurify / isomorphic-dompurify / sanitize-html for JS; bleach for Python). Reject regex-based sanitizers in code review. If unavoidable, a regex sanitizer must:
    • Treat
      [/\s]
      (not just
      \s
      ) as the attribute-name separator — HTML accepts
      /
      between tag name and first attribute:
      <img/onerror=…>
    • Strip both SVG- and HTML-namespace dangerous elements (
      <img>
      ,
      <body>
      ,
      <video>
      ,
      <iframe>
      ) — HTML elements instantiate even in SVG-rendering contexts
    • Include a final fallback pass that strips any
      on*=
      regardless of surrounding context
    • Be paired with
      Content-Security-Policy: script-src-attr 'none'
      as a browser-level backstop
  • SVG uploads as stored XSS. SVG files can carry
    <script>
    /
    onload
    . Most blob / object storage serves uploads with the declared content-type. Reject
    image/svg+xml
    in upload allow-lists unless you have a sanitizer (e.g. DOMPurify SVG profile) and serve with
    Content-Disposition: attachment
    .
  • Sanitize on write AND on render. For stored XSS / injection, sanitize at the trust boundary (write to DB) AND at the render boundary (defense in depth). On finding a stored-XSS bug, plan a one-time backfill migration to sanitize existing data — render-only fixes leave poisoned rows that any new render path will re-expose.
  • Rails JSON-LD breakout: inside
    <script type="application/ld+json">
    , do NOT use
    j
    /
    escape_javascript
    for field values —
    j
    emits
    \'
    and
    \$
    (valid JS, invalid JSON), so
    JSON.parse
    fails on any field containing an apostrophe or
    $
    . Use this idiom instead:
    erb
    <% 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
    handles JSON escaping;
    json_escape
    covers
    <>&\u2028\u2029
    against
    </script>
    breakout. Verify with round-trip:
    JSON.parse(json_escape(article.to_json))
    equals the source hash.
  • Template injection: user input in template literals
  • Grep for:
    exec(
    ,
    eval(
    ,
    innerHTML
    ,
    dangerouslySetInnerHTML
    ,
    $where
    , raw SQL strings
  • 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
    一起搜索这些关键词。
  • ** sanitizer选择**:修复HTML/SVG XSS风险点时,必须使用经过验证的基于解析器的sanitizer(JS使用DOMPurify/isomorphic-dompurify/sanitize-html;Python使用bleach)。代码评审中拒绝基于正则表达式的sanitizer。若无法避免,正则表达式sanitizer必须满足:
    • [/\s]
      (而非仅
      \s
      )视为属性名称分隔符——HTML允许标签名与第一个属性之间使用
      /
      <img/onerror=…>
    • 移除SVG和HTML命名空间中的危险元素(
      <img>
      <body>
      <video>
      <iframe>
      )——HTML元素即使在SVG渲染环境中也会实例化
    • 包含最终回退步骤,无论上下文如何都移除所有
      on*=
      属性
    • 搭配
      Content-Security-Policy: script-src-attr 'none'
      作为浏览器层面的后备防御
  • SVG上传导致存储型XSS:SVG文件可包含
    <script>
    /
    onload
    代码。大多数对象存储会按声明的内容类型提供上传文件。除非使用sanitizer(如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
    时失败。应使用以下写法:
    erb
    <% 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: Insecure Design

A04: Insecure Design(不安全设计)

  • Authentication flows with logic flaws
  • Missing rate limiting on sensitive endpoints (login, password reset, API)
  • Business logic constraints only enforced client-side
  • Background / fire-and-forget jobs inherit the caller's auth context but lose the request-scoped guards. Re-check authorization inside the job, not just at enqueue. Grep for:
    Promise.all(...).catch(
    ,
    void someAsync(
    ,
    .catch(noop)
    , queue
    enqueue(
    without re-auth in the worker.
  • Sister-route audit. When you find a state-machine or immutability guard on one handler (e.g.,
    WHERE … AND signedAt IS NULL
    on
    PUT /api/foo/[id]
    ), grep for every other handler that writes the same table:
    bash
    rg 'update\(\s*tableName\b|\.update\(tableName' --type ts -B1 -A8
    Each call site needs the same guard, the same
    userId
    predicate, and the same conflict-handling (
    returning()
    + 0-rows check). Common offender: a
    POST /:id/send
    or
    POST /:id/convert
    route that ships after the
    PUT
    was hardened and was never re-audited.
  • External-resource-create TOCTOU with billing implications. Any handler that does "SELECT to check, then
    provider.create()
    , then INSERT to record the new resource ID" can create orphan resources on the provider side under concurrency. Stripe accounts, Auth0 / Clerk users, SendGrid templates, S3 buckets — all bill or count toward quota whether you stored the ID or not. Fix pattern:
    1. Claim first with
      INSERT … ON CONFLICT DO NOTHING
      (DB UNIQUE constraint is the lock)
    2. Call the provider
    3. Persist with optimistic guard:
      UPDATE … SET externalId = ? WHERE externalId IS NULL
      and check 0-rows
    4. On race-loss, clean up the orphan via
      provider.delete(id)
      best-effort; log on cleanup failure
  • Worker-queue state transitions need atomic claim. Any cron / worker polling pending rows must atomically claim each row before processing.
    SELECT
    +
    process()
    +
    UPDATE
    is a race — two workers (or two overlapping cron invocations) both see the same pending row and both call out, causing duplicate delivery. Fix:
    UPDATE … SET status='processing' WHERE id=? AND status='pending' RETURNING …
    — Postgres
    RETURNING
    lets you claim and read in one round-trip. If the UPDATE returns 0 rows, someone else got it. Alternative:
    SELECT … FOR UPDATE SKIP LOCKED
    (Postgres / Cockroach) for higher-throughput queues.
  • Multi-tenant webhook signature matching. When an unauthenticated webhook endpoint identifies its tenant by trying each tenant's secret in turn, every request — including garbage — does O(N) DB lookups + O(N) HMAC computations. Attackers flood with random signatures and amplify CPU/DB load without ever passing auth. Defences (compose them):
    1. Signature-shape prefilter before any DB work — reject signatures that aren't the exact length/charset the provider sends (e.g.,
      /^[a-f0-9]{64}$/i
      for HMAC-SHA256 hex)
    2. Hard cap on per-request signature checks (e.g.,
      LIMIT 200
      )
    3. Per-IP rate limit on the endpoint
    4. If the provider supports it, embed the tenant ID in the webhook URL (
      /api/webhooks/foo/<connection_id>
      ) so lookup is O(1)
  • Rate-limit key fallback. If your rate-limit key includes an attacker-controllable or potentially-missing identifier (IP, user-id, session-id), do NOT fall back to a shared constant string when it's absent. Either (a) refuse the request, (b) fall back to a per-resource identifier the attacker can't share (per-email for signup, per-Stripe-customer for billing), or (c) explicitly fail-open and log. A shared
    'unknown'
    /
    'anon'
    bucket is a lockout vector — one attacker pinning the bucket locks out every user behind that proxy path.
  • Configured-but-not-loaded check. Before declaring a security middleware (rate-limit, auth, CSRF, throttle) as "already configured," verify the gem/package is actually installed — not just that the initializer file exists. Initializers wrapped in
    if defined? Foo
    /
    if PACKAGE in sys.modules
    silently no-op when the package isn't bundled.
    StackCheck
    Ruby/Rails
    grep -E "^    GEM_NAME " Gemfile.lock
    (4-space indent = top-level gems)
    Node
    grep "\"PACKAGE_NAME\":" package-lock.json
    or
    node -e "require('PACKAGE_NAME')"
    Python
    pip show PACKAGE_NAME
    Go
    grep PACKAGE_PATH go.sum
    For Rails: also verify the middleware is in the runtime stack —
    bundle exec rails middleware | grep -i FOO
    .
  • 身份验证流程存在逻辑缺陷
  • 敏感端点(登录、密码重置、API)缺少速率限制
  • 业务逻辑约束仅在客户端强制执行
  • 后台/即发即弃任务继承调用者的授权上下文,但丢失请求范围的防护。需在任务内部重新检查授权,而非仅在入队时检查。搜索关键词:
    Promise.all(...).catch(
    void someAsync(
    .catch(noop)
    、队列
    enqueue(
    但在工作进程中未重新验证授权。
  • 关联路由审计:当发现某个处理程序存在状态机或不可变防护(如
    PUT /api/foo/[id]
    中的
    WHERE … AND signedAt IS NULL
    ),需搜索所有写入同一表的其他处理程序:
    bash
    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
      声明(DB唯一约束作为锁)
    2. 调用提供商接口
    3. 使用乐观防护持久化:
      UPDATE … SET externalId = ? WHERE externalId IS NULL
      并检查是否影响0行
    4. 竞争失败时,尽力通过
      provider.delete(id)
      清理孤立资源;清理失败时记录日志
  • 工作队列状态转换需要原子声明:任何轮询待处理行的定时任务/工作进程必须在处理前原子性声明每行。
    SELECT
    +
    process()
    +
    UPDATE
    存在竞争——两个工作进程(或两个重叠的定时任务调用)会同时看到同一待处理行并发起调用,导致重复执行。修复方案:
    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)次DB查询+O(N)次HMAC计算。攻击者可通过随机签名发起洪水攻击,在未通过认证的情况下放大CPU/DB负载。防御措施(可组合使用):
    1. 签名格式预过滤:在任何DB操作前拒绝不符合提供商发送的精确长度/字符集的签名(如HMAC-SHA256十六进制签名使用
      /^[a-f0-9]{64}$/i
    2. 单请求签名检查硬限制(如
      LIMIT 200
    3. 端点的每IP速率限制
    4. 若提供商支持,在Webhook URL中嵌入租户ID
      /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/Rails
    grep -E "^    GEM_NAME " Gemfile.lock
    (4空格缩进=顶级gem)
    Node
    grep "\"PACKAGE_NAME\":" package-lock.json
    node -e "require('PACKAGE_NAME')"
    Python
    pip show PACKAGE_NAME
    Go
    grep PACKAGE_PATH go.sum
    对于Rails:还需验证中间件是否在运行时栈中——
    bundle exec rails middleware | grep -i FOO

A05: Security Misconfiguration

A05: Security Misconfiguration(安全配置错误)

  • Debug mode enabled in production configs
  • Overly permissive CORS policies (
    Access-Control-Allow-Origin: *
    )
  • Missing HTTP security headers (CSP, HSTS, X-Frame-Options, X-Content-Type-Options)
  • Default credentials or configurations shipped
  • Verbose error messages exposing stack traces or internals — including validation libraries echoing schema details (e.g. Zod
    err.issues
    , Joi error trees) to clients
  • Baseline header starter values (paste-and-tune):
    HeaderValue
    Strict-Transport-Security
    max-age=63072000; includeSubDomains; preload
    — preload requires verifying every
    *.example.com
    serves HTTPS first
    X-Content-Type-Options
    nosniff
    X-Frame-Options
    DENY
    (defence-in-depth alongside CSP
    frame-ancestors
    )
    Referrer-Policy
    strict-origin-when-cross-origin
    Permissions-Policy
    camera=(), microphone=(), geolocation=(), browsing-topics=()
    — note
    interest-cohort
    is the legacy FLoC name (Chrome ≤ 100); current Chrome uses
    browsing-topics
    Content-Security-Policy
    start with
    frame-ancestors 'self'
    ; full CSP needs per-site script audit (inline
    <style>
    , JSON-LD, analytics)
    HSTS preload submission is sticky — removal takes months. Verify before submitting.
  • Where security headers live, by framework:
    • Next.js:
      next.config.{js,ts}
      headers()
      block;
      vercel.json
      headers
    • Rails:
      config/initializers/secure_headers.rb
      ,
      config/application.rb
    • Express:
      app.use(helmet())
    • Django:
      SECURE_*
      settings in
      settings.py
  • Runtime-API mismatch. Code running in Edge / Workers / V8-isolate runtimes can't load Node-only modules. Imports of
    node:crypto
    ,
    node:fs
    ,
    node:buffer
    ,
    node:net
    , etc. inside Next.js
    middleware.ts
    / Cloudflare Workers / Vercel Edge functions compile cleanly and fail at first request with
    Failed to load external module
    . Audit middleware and edge-marked routes for Node-only imports; prefer Web Crypto (
    crypto.subtle
    ) for portable code
  • Rails admin-engine mounts. Grep
    config/routes.rb
    for engine and dashboard mounts (PgHero, Sidekiq::Web, Flipper UI, Mission Control, Audit1984) and verify the auth middleware in the corresponding initializer applies in every environment reachable from the internet — not just production:
    bash
    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"
    The README examples for PgHero, Sidekiq::Web, Flipper, and Mission Control all wrap auth in
    if Rails.env.production?
    , which leaves staging, review apps, and preview deploys serving the admin UI anonymously. Fix shape: switch the guard to
    unless Rails.env.local?
    (Rails 7.1+ helper for
    development || test
    ), and add a fail-closed check that refuses access when the auth env vars are unset.
  • Concurrent-execution races on paid endpoints. When an endpoint triggers paid downstream work (LLM calls, third-party APIs, scrape jobs), look for
    SELECT
    followed by a
    runX()
    call without an intervening atomic claim, and unconditional
    UPDATE … SET status='processing'
    writes. Two concurrent requests can both pass the read-side check and both run. Fix: conditional
    UPDATE … WHERE id = ? AND status = 'pending' RETURNING …
    , or a Postgres advisory lock. Charge rate-limit budget only on a successful claim so polling and retries don't burn quota.
(Note: This bullet lives under A05 because the failure manifests as a misconfigured invariant guard. The race pattern itself spans A04 / A05 / A08 depending on framing.)
  • Source-tree hygiene. Grep for sync-conflict duplicates and dead code that could let a reviewer fix the canonical file while leaving a vulnerable copy in place:
    bash
    find . \( -name '* 2.*' -o -name '* 3.*' -o -name '*.orig' \
         -o -name '*~' -o -name '*.bak' \) -not -path '*/node_modules/*'
    Treat findings as A05 — the canonical file may be patched while the duplicate retains the vulnerability.
  • Next.js
    headers()
    rule merging.
    Rules in
    next.config.ts
    headers()
    match per route and merge — a more-specific rule does not override headers it doesn't redeclare. Shipping
    frame-ancestors 'none'
    +
    X-Frame-Options: DENY
    in a
    /:path*
    default plus
    frame-ancestors *
    in a
    /embed
    override results in both
    frame-ancestors *
    and
    X-Frame-Options: DENY
    on the embed route (contradicting; older browsers may break framing). Verify with
    curl -I
    against the deployed origin — config inspection alone misses the merge. Either set XFO in every rule or drop it entirely (CSP
    frame-ancestors
    supersedes on modern browsers).
  • Auth middleware that doesn't exempt bearer/HMAC routes. Cron jobs, Stripe / GitHub webhooks, and any route that authenticates via bearer token or HMAC need to be excluded from session-cookie middleware. Symptom: those routes silently 302 to
    /login
    on every deploy or scheduled invocation, the route-level signature check never runs, and the integration appears to "work" until it doesn't.
  • Bearer-token compare with unset env interpolation. Comparisons that interpolate
    process.env.X
    without a presence check —
    \
    Bearer ${process.env.WEBHOOK_TOKEN}`
    — resolve to a literal
    "Bearer undefined"
    when the env var is missing. An attacker who guesses the env-not-set condition can replay that literal string. Assert presence at module load:
    if (!process.env.WEBHOOK_TOKEN) throw new Error(...)`.
  • API routes returning HTML 302 redirects instead of JSON 401. Auth middleware that 302s every unauthenticated request to
    /login
    breaks
    fetch
    clients (they follow the redirect and consume HTML), obscures auth state in monitoring, and lets attackers learn endpoint existence by 302 vs 404. API routes should return
    401 application/json
    with a machine-readable body.
  • 生产配置中启用调试模式
  • 过于宽松的CORS策略(
    Access-Control-Allow-Origin: *
  • 缺少HTTP安全头(CSP、HSTS、X-Frame-Options、X-Content-Type-Options)
  • 附带默认凭据或配置
  • 详细错误消息暴露堆栈跟踪或内部信息——包括验证库向客户端回显模式细节(如Zod
    err.issues
    、Joi错误树)
  • 基准安全头起始值(可直接复制后调整):
    头信息
    Strict-Transport-Security
    max-age=63072000; includeSubDomains; preload
    ——preload需先验证所有
    *.example.com
    都支持HTTPS
    X-Content-Type-Options
    nosniff
    X-Frame-Options
    DENY
    (与CSP
    frame-ancestors
    配合实现深度防御)
    Referrer-Policy
    strict-origin-when-cross-origin
    Permissions-Policy
    camera=(), 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),并验证对应初始化程序中的授权中间件在所有可从互联网访问的环境中都生效——而非仅在生产环境:
    bash
    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?
    中,这会导致 staging、评审应用和预览部署匿名提供管理UI。修复方式:将防护切换为
    unless Rails.env.local?
    (Rails 7.1+的
    development || test
    辅助函数),并添加失败关闭检查,当授权环境变量未设置时拒绝访问。
  • 付费端点的并发执行竞争:当端点触发付费下游工作(LLM调用、第三方API、爬虫任务)时,查找
    SELECT
    后直接调用
    runX()
    且未进行原子声明的代码,以及无条件执行
    UPDATE … SET status='processing'
    的代码。两个并发请求可能同时通过读取检查并执行操作。修复方案:条件
    UPDATE … WHERE id = ? AND status = 'pending' RETURNING …
    ,或使用Postgres advisory锁。仅在成功声明后才扣除速率限制配额,避免轮询和重试消耗配额。
(注:此条目归属于A05,因为故障表现为不变量防护配置错误。竞争模式本身可归属于A04/A05/A08,取决于场景。)
  • 源码树卫生检查:搜索同步冲突副本和死代码,避免评审者修复规范文件但留下易受攻击的副本:
    bash
    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 *
    ,会导致嵌入路由同时存在
    frame-ancestors *
    X-Frame-Options: DENY
    (相互矛盾;旧浏览器可能无法正常显示嵌入内容)。需通过
    curl -I
    针对部署源进行验证——仅检查配置会遗漏合并问题。要么在每个规则中设置XFO,要么完全移除它(现代浏览器中CSP
    frame-ancestors
    会取代XFO)。
  • 未豁免Bearer/HMAC路由的授权中间件:定时任务、Stripe/GitHub Webhook以及任何通过Bearer令牌或HMAC认证的路由需要从会话Cookie中间件中排除。症状:这些路由在每次部署或定时调用时会静默302重定向到
    /login
    ,路由级签名检查从未执行,集成看似“正常”直到出现故障。API路由应返回
    401 application/json
    及机器可读的响应体。
  • 未设置环境变量时的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 vs 404判断端点是否存在。API路由应返回
    401 application/json
    及机器可读的响应体。

A06: Vulnerable Components

A06: Vulnerable Components(易受攻击组件)

  • Run
    npm audit
    (Node),
    pip audit
    (Python), or equivalent
  • Check lock files for known vulnerable dependency versions
  • Flag dependencies with critical CVEs
  • Run
    npm audit --omit=dev
    alongside
    npm audit
    and triage by reachability:
    • Runtime-reachable (in
      dependencies
      ) — must fix
    • Build-time-only (Vite, esbuild via drizzle-kit, postcss) — usually defer
    • Dev-only (linters, test libs) — defer
  • For the full CVE picture and triage by reachability, invoke
    dependency-audit
    . A06 here is a one-line sanity check.
  • 运行
    npm audit
    (Node)、
    pip audit
    (Python)或等效命令
  • 检查锁文件中是否存在已知易受攻击的依赖版本
  • 标记存在严重CVE的依赖
  • 同时运行
    npm audit --omit=dev
    npm audit
    ,并按可达性分类处理:
    • 运行时可达(在
      dependencies
      中)——必须修复
    • 仅构建时使用(Vite、通过drizzle-kit的esbuild、postcss)——通常可推迟修复
    • 仅开发时使用(代码检查工具、测试库)——可推迟修复
  • 如需完整的CVE情况和按可达性分类处理,调用
    dependency-audit
    。此处A06为一行 sanity check。

A07: Authentication Failures

A07: Authentication Failures(身份验证失败)

  • Weak password policies
  • Session management issues (missing secure/httpOnly flags, no expiry, no rotation)
  • Credential-as-cookie: the cookie value is the credential (e.g.
    cookies.set("admin_token", process.env.ADMIN_PASSWORD)
    and equality-checked on read). Even with
    httpOnly
    and
    secure
    , this is plaintext-credential storage (CWE-522) and lacks rotation/revocation. Replace with an HMAC-signed expiring token verified via
    crypto.subtle.verify
    /
    crypto.timingSafeEqual
  • Non-constant-time credential comparison:
    submitted === expected
    for passwords, API keys, or signatures leaks length and prefix-match via timing. Use
    crypto.timingSafeEqual
    (Node) or
    crypto.subtle.verify
    (Web Crypto)
  • Missing rate limiting on login (credential stuffing risk)
  • Broken password reset flows
  • Rails / Devise —
    password_length
    requires
    :validatable
    .
    config.password_length
    in
    config/initializers/devise.rb
    is only enforced when the User model includes
    :validatable
    in its
    devise :...
    declaration. A model with
    devise :database_authenticatable, :registerable, :recoverable, :rememberable
    (no
    :validatable
    ) accepts passwords of any length and any email format, regardless of what the initializer says. Grep:
    ^\s*devise\s+:
    in
    app/models/
    ; flag any line where
    :validatable
    is absent. Adding it on an existing app validates on create+update but does not retro-invalidate existing weak passwords.
  • NextAuth v5 / Auth.js footguns:
    • AUTH_SECRET
      unset silently derives a weak dev value — assert presence at module load:
      if (!process.env.AUTH_SECRET) throw ...
    • Credentials provider
      authorize()
      has no built-in rate limit — wrap or add upstream limiter; otherwise credential stuffing is trivial
    • Account-existence enumeration via signup error strings ("email already exists") — return a uniform "check your email" response
    • JWT strategy + DrizzleAdapter / PrismaAdapter — the adapter is a near no-op in this combination; revocation must be designed in explicitly
  • Grep for:
    cookies.set\(.*process\.env
    ,
    === .*PASSWORD
    ,
    !== .*SECRET
    ,
    !== .*Bearer.*\${
  • 弱密码策略
  • 会话管理问题(缺少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
    config/initializers/devise.rb
    中的
    config.password_length
    仅当User模型在
    devise :...
    声明中包含
    :validatable
    时才会生效。若模型声明为
    devise :database_authenticatable, :registerable, :recoverable, :rememberable
    (无
    :validatable
    ),则会接受任意长度和格式的密码,无论初始化程序如何设置。搜索关键词:
    ^\s*devise\s+:
    app/models/
    中;标记任何缺少
    :validatable
    的行。在现有应用中添加
    :validatable
    会在创建+更新时进行验证,但不会追溯使现有弱密码失效。
  • NextAuth v5 / Auth.js陷阱
    • AUTH_SECRET
      未设置时会静默生成弱开发环境值——需在模块加载时断言变量存在:
      if (!process.env.AUTH_SECRET) throw ...
    • Credentials provider的
      authorize()
      无内置速率限制——需包装或添加上游限制器;否则凭据填充攻击会非常容易
    • 通过注册错误字符串枚举账户存在性(“邮箱已存在”)——返回统一的“检查你的邮箱”响应
    • JWT策略 + DrizzleAdapter/PrismaAdapter——此组合中适配器几乎无作用;必须显式设计撤销机制
  • 搜索关键词:
    cookies.set\(.*process\.env
    === .*PASSWORD
    !== .*SECRET
    !== .*Bearer.*\${

A08: Data Integrity Failures

A08: Data Integrity Failures(数据完整性问题)

  • Unsafe deserialization of user input
  • Missing integrity checks on CI/CD pipelines
  • No lockfile integrity verification (SRI hashes)
  • External-resource overwrite without state check. Any handler that creates a provider-side resource (Stripe payment intent, subscription, webhook subscription) and stores the ID on a DB row should check whether a prior resource is still in-flight before overwriting. The old
    clientSecret
    / token may still be held by the client; on completion, the webhook handler may not find the row because the ID has changed (money lands, DB sits at
    processing
    forever). Fix: before creating a new resource, retrieve the existing one. If status is non-terminal (
    processing
    ,
    requires_payment_method
    ,
    requires_action
    ) and parameters haven't changed, REUSE it — return the existing clientSecret. Only create a new resource when the prior one is canceled / succeeded or the parameters changed.
  • External side-effects before durable DB state. When a handler both writes to the DB and triggers an external side-effect (email, charge, webhook, S3 write), the external call should come after the DB write commits. The failure mode of "DB durable, external retried" is recoverable; "external done, DB stale" is not. Fix pattern: reserve-then-act. Issue a conditional UPDATE that flips the row to the post-action state guarded by the pre-action state (
    WHERE status='draft'
    ). If 0 rows, refuse. If 1 row, call the provider. On provider failure, the row already reflects intent — alert and retry. Bonus: this also makes the handler idempotent.
  • 用户输入的不安全反序列化
  • CI/CD流水线缺少完整性检查
  • 无锁文件完整性验证(SRI哈希)
  • 无状态检查的外部资源覆盖:任何创建提供商端资源(Stripe支付意向、订阅、Webhook订阅)并将ID存储在DB行中的处理程序,应在覆盖前检查是否存在仍在处理中的先前资源。旧的
    clientSecret
    /令牌可能仍被客户端持有;完成时,Webhook处理程序可能因ID已更改而无法找到该行(资金到账,但DB状态仍为
    processing
    )。修复方案:创建新资源前,检索现有资源。若状态为非终端(
    processing
    requires_payment_method
    requires_action
    )且参数未更改,则重用现有资源——返回现有clientSecret。仅当先前资源已取消/成功或参数更改时才创建新资源。
  • 持久化DB状态前触发外部副作用:当处理程序同时写入DB并触发外部副作用(邮件、收费、Webhook、S3写入)时,外部调用应在DB写入提交后执行。“DB持久化,外部调用重试”的故障模式可恢复;“外部调用完成,DB状态陈旧”的故障模式不可恢复。修复模式:预留后执行。执行条件UPDATE,将行状态从预操作状态切换为后操作状态(
    WHERE status='draft'
    )。若影响0行,则拒绝请求。若影响1行,则调用提供商接口。提供商调用失败时,行已反映意图——触发警报并重试。额外优势:此方式还能使处理程序具备幂等性。

A09: Logging & Monitoring Failures

A09: Logging & Monitoring Failures(日志与监控失败)

  • Auth events not logged (login, failure, privilege changes)
  • Sensitive data written to logs (passwords, tokens, PII)
  • No alerting on suspicious patterns
  • Silent error swallowing. Empty catch blocks hide both bugs and attack signals (rate limiter falling over, deserialization errors, auth failures).
    • Grep for:
      catch \{\}
      ,
      catch (_) \{\}
      ,
      catch (_e) \{\}
      ,
      .catch(() => {})
      ,
      .catch(() => null)
      ,
      try { ... } catch { return null }
    • Fix: at minimum log the error category (
      error.name
      , status code) without PII
  • Unauthenticated endpoints sending email from a verified domain to user-supplied addresses. Any handler reachable without auth that triggers an outbound email — confirmation, password-reset to-be-claimed, invite, "we got your message" — to an attacker-supplied
    to:
    address, especially with attacker-influenced subject/body, is a phishing vector against your verified sender's deliverability reputation. Defences (pick one):
    1. Don't auto-confirm — replies happen when a human responds
    2. Require ownership proof before any reply email (verification link / double opt-in)
    3. Use a separate, plainly-templated
      noreply@
      sender with subject/body the attacker can't influence
  • 未记录认证事件(登录、失败、权限变更)
  • 敏感数据写入日志(密码、令牌、PII)
  • 对可疑模式无告警机制
  • 静默错误吞噬:空catch块会隐藏漏洞和攻击信号(速率限制器故障、反序列化错误、认证失败)。
    • 搜索关键词:
      catch \{\}
      catch (_) \{\}
      catch (_e) \{\}
      .catch(() => {})
      .catch(() => null)
      try { ... } catch { return null }
    • 修复方案:至少记录错误类别(
      error.name
      、状态码)且不包含PII
  • 未认证端点从已验证域名向用户提供的地址发送邮件:任何无需认证即可访问的处理程序,若向攻击者提供的
    to:
    地址触发出站邮件(确认邮件、待认领的密码重置邮件、邀请邮件、“已收到你的消息”邮件),且邮件主题/内容受攻击者影响,则会成为针对已验证发件人交付信誉的钓鱼载体。防御措施(选择其一):
    1. 不自动确认——仅当人工回复时才发送邮件
    2. 在发送回复邮件前要求所有权证明(验证链接/双重选择加入)
    3. 使用单独的、模板固定的
      noreply@
      发件人,且主题/内容不受攻击者影响

A10: SSRF

A10: SSRF(服务器端请求伪造)

  • User-controlled URLs passed to server-side HTTP requests
  • Missing URL validation and allowlisting
  • Allow-lists that only check hostname — a real allow-list must reject all of these:
    • Wrong scheme:
      http://allowed.com/
      (when only
      https:
      is expected)
    • Embedded credentials:
      https://user:pass@allowed.com/
    • @
      -host trick:
      https://allowed.com@evil.com/
      (hostname resolves to
      evil.com
      )
    • Non-default ports:
      https://allowed.com:8443/
    • Punycode/IDN spoof:
      https://аllowed.com/
      (Cyrillic а) or
      xn--llowed-pdc.com
    • Trailing dot:
      https://allowed.com./
      (DNS-equivalent, often missed by string compare)
    • Subdomain confusion:
      https://allowed.com.evil.com/
    • Bracketed IPv6 literal:
      https://[::1]/
    • Bare IPv4:
      https://127.0.0.1/
    • Decimal-integer IPv4:
      http://2130706433/
      → 127.0.0.1
    • Hex IPv4:
      http://0x7f000001/
    • Octal IPv4:
      http://0177.0.0.1/
      ; zero-padded
      0010.0.0.1/
      → 8.0.0.1 (octal!)
    • IPv4-mapped IPv6:
      http://[::ffff:127.0.0.1]/
      → block the whole
      ::ffff:*
      range
    • Trailing-dot hostname:
      http://localhost./
      ,
      http://metadata.google.internal./
    • Cloud metadata endpoints: AWS
      169.254.169.254
      , GCP
      metadata.google.internal
      , ECS
      169.254.170.2
    • CGNAT range:
      100.64.0.0
      100.127.255.255
    • Link-local IPv6:
      fe80::/10
      ; unique-local IPv6:
      fc00::/7
  • Fetch-time guards:
    redirect: "error"
    (don't follow attacker-controlled redirects), explicit timeout, no following 3xx into the metadata service
  • Note the TOCTOU between validation and fetch — DNS can resolve differently between the two (DNS rebinding). For high-risk callers, pin the resolved IP and connect by IP with
    Host:
    header, or use a vetted proxy
  • Image-optimizer-as-proxy (Next.js, Nuxt, SvelteKit):
    next.config.{ts,js}
    with
    images.remotePatterns: [{ hostname: '**' }]
    ,
    domains: ['*']
    , or any wildcard entry lets attackers route arbitrary URLs through your CPU/bandwidth.
    • Grep for:
      remotePatterns
      ,
      domains:
      in image config
    • Fix: pin to specific known hostnames; leave empty if all images are local.
  • Grep for:
    fetch(
    ,
    axios(
    ,
    http.get(
    ,
    urllib
    ,
    requests.get(
    with user input
  • 用户可控URL传入服务器端HTTP请求
  • 缺少URL验证和允许列表
  • 仅检查主机名的允许列表无效——真正的允许列表必须拒绝以下所有情况:
    • 错误协议:
      http://allowed.com/
      (预期仅使用
      https:
    • 嵌入凭据:
      https://user:pass@allowed.com/
    • @
      主机欺骗:
      https://allowed.com@evil.com/
      (主机解析为
      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 rebinding)。对于高风险调用者,固定解析后的IP并通过IP连接同时设置
    Host:
    头,或使用经过验证的代理。
  • 图片优化器作为代理(Next.js、Nuxt、SvelteKit):
    next.config.{ts,js}
    中设置
    images.remotePatterns: [{ hostname: '**' }]
    domains: ['*']
    或任何通配符条目,会让攻击者通过你的CPU/带宽路由任意URL。
    • 搜索关键词:
      remotePatterns
      domains:
      在图片配置中
    • 修复方案:固定到特定已知主机名;若所有图片均为本地则留空。
  • 搜索关键词:
    fetch(
    axios(
    http.get(
    urllib
    requests.get(
    搭配用户输入

Verify Fixes at Runtime

运行时验证修复

After applying a fix, exercise the affected code path — do not stop at typecheck or build. Modern frameworks have runtime-only failure modes that compile cleanly:
  • Edge / Node split runtimes. Next.js middleware, Cloudflare Workers, Vercel Edge — Node-only imports (
    node:crypto
    ,
    node:fs
    ) build successfully but throw on first request.
  • Lazy module loads. Adapters/plugins loaded via
    import()
    or runtime DI surface only when the codepath runs.
  • Environment-variable fallthrough.
    Bearer ${process.env.X}
    with X unset becomes a literal that the tests never hit because the test env defines X.
For each shipped fix, run the affected route or job and capture the response.
tsc --noEmit
+ build success ≠ fix verified.
For XSS / sanitizer-config fixes, run the canonical payload set through the configured policy and confirm each is neutralized:
<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>
应用修复后,需测试受影响的代码路径——不要仅停留在类型检查或构建阶段。现代框架存在仅运行时才会暴露的故障模式,且编译时无错误:
  • Edge/Node分离运行时:Next.js中间件、Cloudflare Workers、Vercel Edge——仅Node可用的导入(
    node:crypto
    node:fs
    )编译成功但首次请求时会抛出错误。
  • 懒加载模块:通过
    import()
    或运行时DI加载的适配器/插件仅在代码路径执行时才会暴露问题。
  • 环境变量传递问题
    Bearer ${process.env.X}
    在X未设置时会成为字面量,而测试环境中X已定义,因此测试不会覆盖此场景。
对于每个已部署的修复,运行受影响的路由或任务并捕获响应。
tsc --noEmit
+ 构建成功 ≠ 修复已验证。
对于XSS/sanitizer配置修复,通过配置的策略运行标准载荷集并确认每个载荷都被中和:
<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>

Second-Opinion Pass

二次评审

A single-pass audit reliably catches the categories on the checklist but misses the specific bypasses that aren't in the checklist (
localhost.
vs
localhost
, IPv4-mapped IPv6, status-enumeration via ordered 404/400/401, callback-URL control chars, concurrent-execution races on paid endpoints).
After producing the first report AND after applying fixes, run a second pass with explicit adversarial framing ("assume the author is overconfident; find what they missed") — ideally with a different model or agent entirely, to break correlated blind spots. Treat any disagreement with the first pass as the higher-value finding.
Common things to find in the second pass:
  • New attack surface introduced by the fix itself (auth bypass via exempted routes, IDOR introduced by a new query)
  • Comments that became stale during the rewrite
  • Boundary conditions in the new code (env-unset fallthrough, empty input)
  • Documentation drift between the fix and the report
  • Fixes that configure third-party libraries. When the fix is a snippet from a library's own docs (auth, crypto, HTTP client, rate-limit middleware), the snippet may be correct and still not run on your code path. Before declaring fixed: grep the library at the pinned version, trace from your call site to the code path the config affects. Example: enabling Better Auth's
    rateLimit
    doesn't help if you call
    auth.api.signInEmail(...)
    programmatically — that bypasses the HTTP router where the limiter attaches.
单次审计可可靠捕获检查清单中的类别,但会遗漏清单未提及的特定绕过方式(
localhost.
vs
localhost
、IPv4映射IPv6、通过有序404/400/401枚举状态、回调URL控制字符、付费端点的并发执行竞争)。
生成第一份报告并应用修复后,以明确的对抗性框架进行二次评审(“假设作者过于自信;找出他们遗漏的内容”)——理想情况下使用不同模型或代理,打破关联盲点。将与第一次评审的任何分歧视为更有价值的发现。
二次评审中常见的发现:
  • 修复本身引入的新攻击面(豁免路由导致的认证绕过、新查询引入的IDOR)
  • 重写过程中过时的注释
  • 新代码中的边界条件(环境变量未设置传递、空输入)
  • 修复与报告之间的文档偏差
  • 配置第三方库的修复:当修复是库文档中的代码片段(认证、加密、HTTP客户端、速率限制中间件)时,片段可能正确但仍未在你的代码路径中运行。在声明修复完成前:搜索固定版本的库代码,从你的调用点追踪到配置影响的代码路径。示例:启用Better Auth的
    rateLimit
    对通过
    auth.api.signInEmail(...)
    编程式调用无帮助——这会绕过速率限制器附加的HTTP路由器。

Report Format

报告格式

For every OWASP category, document one of three states:
  • Findings (with severity + remediation)
  • Clean — explicitly state "Checked X, found no issues" with what you grepped for
  • N/A — explain why the category doesn't apply (e.g. "A07 N/A: no authentication surface in this codebase")
Include an "Items checked and found clean" section in the executive summary. Audit credibility comes from proving you looked, not just from the findings list.
Findings have three possible dispositions:
  • Fixed — remediation shipped in this PR. Closed pending verification.
  • Deferred — remediation acknowledged and scheduled. Specify whether the next deploy is gated on it (release blocker) or not (acceptable risk with calendar fix). Severity does not change because you decided to defer it.
  • Accepted Risk — remediation is NOT planned at the current configuration. The report must record:
    1. Why the fix doesn't apply — cost tier, dependency version constraint, deployment topology, vendor limitation
    2. Compensating controls — private network, signed cookies, internal-only routing, etc.
    3. Re-evaluation trigger — what condition (plan upgrade, dependency bump, traffic pattern change) would cause this finding to leave the Accepted Risk lane
An "Accepted Risk" entry without all three fields is a real finding being silently dropped under a different label.
For each finding, document:
markdown
undefined
对于每个OWASP类别,记录以下三种状态之一:
  • 发现问题(包含严重性+修复方案)
  • 无问题——明确说明“检查了X,未发现问题”及搜索的关键词
  • 不适用——解释类别不适用的原因(如“A07不适用:此代码库无认证接口”)
在执行摘要中包含“已检查且无问题的项目”部分。审计的可信度来自证明你已检查,而非仅列出发现的问题。
发现的问题有三种处理状态:
  • 已修复——修复已在本次PR中部署。待验证后关闭。
  • 推迟修复——确认修复并已安排时间。指定是否阻止下一次部署(发布阻塞)或不阻止(可接受风险并计划在指定日期修复)。严重性不会因决定推迟而改变。
  • 接受风险——当前配置下不计划修复。报告必须记录:
    1. 修复不适用的原因——成本层级、依赖版本约束、部署拓扑、供应商限制
    2. 补偿控制措施——私有网络、签名Cookie、仅内部路由等
    3. 重新评估触发条件——什么条件(套餐升级、依赖版本更新、流量模式变化)会导致此发现退出“接受风险”类别
缺少以上三个字段的“接受风险”条目是被悄悄换标签的真实问题。
每个发现的问题需记录:
markdown
undefined

[SEVERITY] A0X: [Title]

[严重性] A0X: [标题]

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.

Produce an executive summary:

```markdown
文件:
path/to/file.ts:42
CWE: CWE-XXX
描述: [漏洞是什么以及为何重要]
易受攻击代码: [代码片段]
修复方案: [带解释的修复后代码片段]
验证: 具体的对抗性输入 + 证明修复有效的命令或代码路径。对于XSS:不再突破的脚本标签载荷。对于开放重定向:现在被拒绝的外部URL。对于密码长度:现在无法保存的1字符密码。“代码检查工具显示没问题”不是验证——静态分析对于同时也是安全修复的正确性漏洞存在已知盲点。

生成执行摘要:

```markdown

Security Audit Report

安全审计报告

Project: [name]

项目: [名称]

Stack: [technologies]

技术栈: [技术]

Date: [date]

日期: [日期]

Summary

摘要

  • Total findings: X
  • Critical: X | High: X | Medium: X | Low: X | Info: X
  • 总发现问题数: X
  • 严重: X | 高: X | 中: X | 低: X | 信息: X

Findings

发现问题

[Individual findings as above]
[上述单个问题记录]

Prioritized Remediation Plan

优先修复计划

  1. [Critical fixes — immediate]
  2. [High fixes — this week]
  3. [Medium/Low — scheduled]
undefined
  1. [严重修复——立即处理]
  2. [高优先级修复——本周内处理]
  3. [中/低优先级——已安排]
undefined

Boundaries

边界

  • Only audit code the user provides or points you to
  • Provide fixes, not exploits — always include remediation
  • Flag low-confidence findings as "Potential" rather than confirmed
  • If the codebase is too large for a full audit, prioritize: auth, input handling, data access layers
  • Refuse requests to insert backdoors or weaken security controls
  • 仅审计用户提供或指向的代码
  • 提供修复方案,而非漏洞利用方法——始终包含修复建议
  • 将低可信度发现标记为“潜在”而非已确认
  • 若代码库过大无法全面审计,优先处理:认证、输入处理、数据访问层
  • 拒绝插入后门或削弱安全控制的请求

References

参考资料

  • OWASP Top 10 (2021)
  • OWASP Code Review Guide
  • CWE Top 25
  • OWASP Top 10 (2021)
  • OWASP代码评审指南
  • CWE Top 25