owasp-audit
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseOWASP 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
审计范围界定
- Identify the project's language, framework, and architecture
- Map entry points (routes, API handlers, form processors)
- Identify data flows (user input → processing → storage → output)
- Locate authentication and authorization boundaries
- 确定项目的语言、框架和架构
- 映射入口点(路由、API处理程序、表单处理器)
- 识别数据流(用户输入→处理→存储→输出)
- 定位身份验证和授权边界
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 /
actionexportloader - tRPC: every procedure
- GraphQL: every resolver
- Rails: non-resource controller actions
- Next.js: every exported function in a file with
- IDOR via foreign keys in mutation payloads. Form posts a foreign-key UUID (,
categoryId,projectId,teamId) → server validates ownership of the primary record but blindly accepts the FK → ORM relation join later surfaces another tenant's data. Look fororganizationId/formData.get("<id>")passed straight to insert/update without a precedingbody.<id>. For ORM relation joins (DrizzlefindFirst({ where: { id, userId } }), Prismawith:, ActiveRecordinclude), trace whether the join target is filtered by the same tenant/ownership predicate as the parent query.includes - 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=passed unsanitized to?redirect=/redirect(). Restrict to same-origin paths under the expected scope, normalize (Response.redirect()) to defeat traversal likenew URL(target, "http://localhost").pathname. Also reject control bytes in the path before redirect: tab/newline/null (/admin/../foo,\t,\n) — URL parsers strip these and collapse\0into protocol-relative/\tevil; null bytes can turn the redirect into a 500. Reject any byte in//evil, any backslash, and any percent-encoded slash/backslash ([\x00-\x1F\x7F],%2f).%5c - Grep for: direct object references, missing auth middleware, user ID from request params, ,
redirect(.*from,redirect(.*nextredirect(.*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:非资源控制器动作
- Next.js:所有包含
- 通过变更负载中的外键实现IDOR:表单提交外键UUID(、
categoryId、projectId、teamId)→服务器验证主记录的所有权但盲目接受外键→后续ORM关联查询暴露其他租户的数据。查找直接将organizationId/formData.get("<id>")传入插入/更新操作且未先执行body.<id>的代码。对于ORM关联查询(DrizzlefindFirst({ where: { id, userId } })、Prismawith:、ActiveRecordinclude),需追踪关联目标是否与父查询使用相同的租户/所有权谓词进行过滤。includes - 状态变更请求缺少CSRF保护
- 仅在前端进行角色检查,未在服务器端强制执行
- 通过认证后的返回参数实现开放重定向——、
?from=、?next=、?returnTo=、?continue=未经过滤就传入?redirect=/redirect()。限制为预期范围内的同源路径,通过标准化(Response.redirect())防止路径遍历,如new URL(target, "http://localhost").pathname。同时在重定向前拒绝路径中的控制字符:制表符/换行符/空字符(/admin/../foo、\t、\n)——URL解析器会剥离这些字符并将\0转换为协议相对路径/\tevil;空字符可能导致重定向返回500错误。拒绝//evil中的任何字节、任何反斜杠以及任何百分比编码的斜杠/反斜杠([\x00-\x1F\x7F]、%2f)。%5c - 搜索关键词:直接对象引用、缺少授权中间件、请求参数中的用户ID、、
redirect(.*from、redirect(.*nextredirect(.*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) silently producesparseFloatfor garbage input, andNaNcompares asNaNfor bothfalseand<. A timestamp-freshness check>fails to rejectif (Math.abs(now - parsed) > tolerance) return false— becauseNaNisNaN > tolerance. Grep for:falseinsideparseInt|parseFloat|Number\(.*\)/verifySignature/ signed-cookie / JWT-claim code. Each numeric extraction must be followed byvalidateTokenbefore any inequality. Same family:if (!Number.isFinite(parsed)) return false,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 recommendingfor 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_PEERfails there without an explicitVERIFY_PEERpin. Whenca_file:is genuinely infeasible, present three remediation options in priority order:VERIFY_PEER- Upgrade the plan or pin the CA bundle — restores cert verification
- Accept the risk explicitly — leave 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
VERIFY_NONE - Restrict the network path — private subnet / VPC peering / no public exposure
Never quietly recommendwithout checking that the cert chain at the deployment target is verifiable.VERIFY_PEER -
Grep for generic secret names AND known provider key prefixes:
- Generic: ,
password,secret,api_key,private_key,MD5,SHA1base64 - 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: , service-account JSON (
AIza[0-9A-Za-z\-_]{35})"type": "service_account" - Slack: ,
xox[baprs]-xoxe.xoxp- - OpenAI / Anthropic: ,
sk-sk-ant- - Vercel:
vercel_blob_rw_ - Run via so binaries and gitignored files don't pollute output.
git ls-files | xargs grep -lE 'sk_live|ghp_|AKIA[0-9A-Z]{16}|sk-ant-' 2>/dev/null
- Generic:
-
Include non-source file extensions in the sweep. Rails/
cable.yml/database.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:storage.ymlbashgrep -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/签名Cookie/JWT声明代码中。每个数值提取后必须在进行任何比较前添加validateToken。同类问题:if (!Number.isFinite(parsed)) return false、parseInt('0x123', 10) === 0、parseInt('1e10', 10) === 1。parseFloat('Infinity') === Infinity -
敏感数据出现在日志、URL或localStorage中
-
静态存储或传输过程中缺少加密
-
在推荐TLS连接使用前,需确认部署目标的证书颁发机构。许多托管服务在低层级套餐中使用自签名证书链(Heroku Redis Mini/Hobby、部分ElastiCache配置、Supabase旧版本)——若无显式
VERIFY_PEER固定证书,ca_file:会验证失败。当确实无法使用VERIFY_PEER时,按优先级提供三种修复方案:VERIFY_PEER- 升级套餐或固定CA证书包——恢复证书验证
- 明确接受风险——保留,同时满足:(a) 每个调用点添加内联注释;(b) 记录补偿控制措施(私有网络、仅内部路由);(c) 创建跟踪重新验证条件的后续任务
VERIFY_NONE - 限制网络路径——私有子网/VPC对等连接/无公网暴露
切勿在未确认部署目标证书链可验证的情况下直接推荐。VERIFY_PEER -
搜索通用密钥名称及已知提供商密钥前缀:
- 通用:、
password、secret、api_key、private_key、MD5、SHA1base64 - 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:、服务账户JSON(
AIza[0-9A-Za-z\-_]{35})"type": "service_account" - Slack:、
xox[baprs]-xoxe.xoxp- - OpenAI / Anthropic:、
sk-sk-ant- - Vercel:
vercel_blob_rw_ - 执行命令:,避免二进制文件和git忽略文件干扰输出。
git ls-files | xargs grep -lE 'sk_live|ghp_|AKIA[0-9A-Z]{16}|sk-ant-' 2>/dev/null
- 通用:
-
扫描范围包含非源代码文件扩展名:Rails的/
cable.yml/database.yml、Kubernetes清单、Vercel/Netlify部署配置通常包含TLS或证书配置,仅扫描源代码会遗漏。扫描storage.yml/VERIFY_NONE的具体命令:VERIFY_PEERbashgrep -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()with user inputsystem() -
XSS: unescaped user input in HTML,,
dangerouslySetInnerHTML.v-html -
Inline-script breakout via. Any
JSON.stringifyor<script type="application/ld+json">that interpolates server data through<script>window.__DATA__ = ...</script>is vulnerable —JSON.stringifydoes NOT escapeJSON.stringify,<,>, U+2028, or U+2029. A stored title containing&will break out. The "internal-only object" framing only saves you when every field is guaranteed never to come from user-editable input.</script><script>alert(1)</script>- Grep for: ,
application/ld+json,__html: JSON.stringify+window.__JSON.stringify - Fix: wrap with an escape helper that replaces with their
<>&\u2028\u2029Unicode escapes before injecting.\uXXXX
- Grep for:
-
Rails ERB sinks:,
raw(),.html_safe,<%==with a permissive allowlist, andsanitizeon user input. Grep for these alongsidesimple_format/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 (not just
[/\s]) as the attribute-name separator — HTML accepts\sbetween tag name and first attribute:/<img/onerror=…> - Strip both SVG- and HTML-namespace dangerous elements (,
<img>,<body>,<video>) — HTML elements instantiate even in SVG-rendering contexts<iframe> - Include a final fallback pass that strips any regardless of surrounding context
on*= - Be paired with as a browser-level backstop
Content-Security-Policy: script-src-attr 'none'
- Treat
-
SVG uploads as stored XSS. SVG files can carry/
<script>. Most blob / object storage serves uploads with the declared content-type. Rejectonloadin upload allow-lists unless you have a sanitizer (e.g. DOMPurify SVG profile) and serve withimage/svg+xml.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, do NOT use
<script type="application/ld+json">/jfor field values —escape_javascriptemitsjand\'(valid JS, invalid JSON), so\$fails on any field containing an apostrophe orJSON.parse. 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>handles JSON escaping;to_jsoncoversjson_escapeagainst<>&\u2028\u2029breakout. Verify with round-trip:</script>equals the source hash.JSON.parse(json_escape(article.to_json)) -
Template injection: user input in template literals
-
Grep for:,
exec(,eval(,innerHTML,dangerouslySetInnerHTML, raw SQL strings$where
-
SQL注入:使用字符串拼接的原生查询,缺少参数化查询
-
NoSQL注入:MongoDB/Convex查询中使用未过滤的用户输入
-
命令注入:、
exec()、spawn()调用中传入用户输入system() -
XSS:HTML中使用未转义的用户输入、、
dangerouslySetInnerHTMLv-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\u2029Unicode转义字符。\uXXXX
- 搜索关键词:
-
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])视为属性名称分隔符——HTML允许标签名与第一个属性之间使用\s:/<img/onerror=…> - 移除SVG和HTML命名空间中的危险元素(、
<img>、<body>、<video>)——HTML元素即使在SVG渲染环境中也会实例化<iframe> - 包含最终回退步骤,无论上下文如何都移除所有属性
on*= - 搭配作为浏览器层面的后备防御
Content-Security-Policy: script-src-attr 'none'
- 将
-
SVG上传导致存储型XSS:SVG文件可包含/
<script>代码。大多数对象存储会按声明的内容类型提供上传文件。除非使用sanitizer(如DOMPurify SVG配置文件)并以onload方式提供,否则拒绝上传允许列表中的Content-Disposition: attachment类型。image/svg+xml -
写入和渲染时均需过滤:针对存储型XSS/注入,需在信任边界(写入数据库)和渲染边界(深度防御)都进行过滤。发现存储型XSS漏洞时,需计划一次性回填迁移以过滤现有数据——仅在渲染时修复会留下中毒数据行,任何新的渲染路径都会重新暴露漏洞。
-
Rails JSON-LD突破问题:在内,请勿对字段值使用
<script type="application/ld+json">/j——escape_javascript会生成j和\'(有效JS但无效JSON),导致包含撇号或\$的字段在$时失败。应使用以下写法:JSON.parseerb<% schema = { "@context" => "https://schema.org", "@type" => "Article", "headline" => @post.title } %> <script type="application/ld+json"> <%= json_escape(schema.to_json).html_safe %> </script>处理JSON转义;to_json覆盖json_escape以防止<>&\u2028\u2029突破。通过往返验证确认:</script>与源哈希值相等。JSON.parse(json_escape(article.to_json)) -
模板注入:模板字面量中使用用户输入
-
搜索关键词:、
exec(、eval(、innerHTML、dangerouslySetInnerHTML、原生SQL字符串$where
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(, queue.catch(noop)without re-auth in the worker.enqueue( -
Sister-route audit. When you find a state-machine or immutability guard on one handler (e.g.,on
WHERE … AND signedAt IS NULL), grep for every other handler that writes the same table:PUT /api/foo/[id]bashrg 'update\(\s*tableName\b|\.update\(tableName' --type ts -B1 -A8Each call site needs the same guard, the samepredicate, and the same conflict-handling (userId+ 0-rows check). Common offender: areturning()orPOST /:id/sendroute that ships after thePOST /:id/convertwas hardened and was never re-audited.PUT -
External-resource-create TOCTOU with billing implications. Any handler that does "SELECT to check, then, 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:
provider.create()- Claim first with (DB UNIQUE constraint is the lock)
INSERT … ON CONFLICT DO NOTHING - Call the provider
- Persist with optimistic guard: and check 0-rows
UPDATE … SET externalId = ? WHERE externalId IS NULL - On race-loss, clean up the orphan via best-effort; log on cleanup failure
provider.delete(id)
- Claim first with
-
Worker-queue state transitions need atomic claim. Any cron / worker polling pending rows must atomically claim each row before processing.+
SELECT+process()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— PostgresUPDATE … SET status='processing' WHERE id=? AND status='pending' RETURNING …lets you claim and read in one round-trip. If the UPDATE returns 0 rows, someone else got it. Alternative:RETURNING(Postgres / Cockroach) for higher-throughput queues.SELECT … FOR UPDATE SKIP LOCKED -
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):
- Signature-shape prefilter before any DB work — reject signatures that aren't the exact length/charset the provider sends (e.g., for HMAC-SHA256 hex)
/^[a-f0-9]{64}$/i - Hard cap on per-request signature checks (e.g., )
LIMIT 200 - Per-IP rate limit on the endpoint
- If the provider supports it, embed the tenant ID in the webhook URL () so lookup is O(1)
/api/webhooks/foo/<connection_id>
- Signature-shape prefilter before any DB work — reject signatures that aren't the exact length/charset the provider sends (e.g.,
-
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'bucket is a lockout vector — one attacker pinning the bucket locks out every user behind that proxy path.'anon' -
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? Foosilently no-op when the package isn't bundled.if PACKAGE in sys.modulesStack Check Ruby/Rails (4-space indent = top-level gems)grep -E "^ GEM_NAME " Gemfile.lockNode orgrep "\"PACKAGE_NAME\":" package-lock.jsonnode -e "require('PACKAGE_NAME')"Python pip show PACKAGE_NAMEGo grep PACKAGE_PATH go.sumFor 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 NULLbashrg 'update\(\s*tableName\b|\.update\(tableName' --type ts -B1 -A8每个调用点都需要相同的防护、相同的谓词以及相同的冲突处理(userId+ 0行检查)。常见问题:returning()或POST /:id/send路由在POST /:id/convert路由加固后开发,未重新进行审计。PUT -
外部资源创建的TOCTOU问题(涉及计费):任何执行“SELECT检查→→INSERT记录新资源ID”的处理程序在并发情况下可能在提供商端创建孤立资源。Stripe账户、Auth0/Clerk用户、SendGrid模板、S3存储桶——无论是否存储ID,都会产生费用或占用配额。修复模式:
provider.create()- 先通过声明(DB唯一约束作为锁)
INSERT … ON CONFLICT DO NOTHING - 调用提供商接口
- 使用乐观防护持久化:并检查是否影响0行
UPDATE … SET externalId = ? WHERE externalId IS NULL - 竞争失败时,尽力通过清理孤立资源;清理失败时记录日志
provider.delete(id)
- 先通过
-
工作队列状态转换需要原子声明:任何轮询待处理行的定时任务/工作进程必须在处理前原子性声明每行。+
SELECT+process()存在竞争——两个工作进程(或两个重叠的定时任务调用)会同时看到同一待处理行并发起调用,导致重复执行。修复方案:UPDATE——Postgres的UPDATE … SET status='processing' WHERE id=? AND status='pending' RETURNING …允许在一次往返中完成声明和读取。若UPDATE返回0行,则说明该行已被其他进程获取。替代方案:RETURNING(Postgres/Cockroach)用于高吞吐量队列。SELECT … FOR UPDATE SKIP LOCKED -
多租户Webhook签名匹配:当未认证的Webhook端点通过依次尝试每个租户的密钥来识别租户时,每个请求(包括垃圾请求)都会执行O(N)次DB查询+O(N)次HMAC计算。攻击者可通过随机签名发起洪水攻击,在未通过认证的情况下放大CPU/DB负载。防御措施(可组合使用):
- 签名格式预过滤:在任何DB操作前拒绝不符合提供商发送的精确长度/字符集的签名(如HMAC-SHA256十六进制签名使用)
/^[a-f0-9]{64}$/i - 单请求签名检查硬限制(如)
LIMIT 200 - 端点的每IP速率限制
- 若提供商支持,在Webhook URL中嵌入租户ID(),使查询变为O(1)
/api/webhooks/foo/<connection_id>
- 签名格式预过滤:在任何DB操作前拒绝不符合提供商发送的精确长度/字符集的签名(如HMAC-SHA256十六进制签名使用
-
速率限制键回退:若速率限制键包含攻击者可控或可能缺失的标识符(IP、用户ID、会话ID),当标识符缺失时请勿回退到共享常量字符串。需选择:(a) 拒绝请求;(b) 回退到攻击者无法共享的每个资源标识符(注册时按邮箱、计费时按Stripe客户);(c) 显式开放失败并记录日志。共享的/
'unknown'桶是锁定向量——攻击者占用该桶会导致该代理路径下的所有用户被锁定。'anon' -
已配置但未加载检查:在声明安全中间件(速率限制、认证、CSRF、节流)“已配置”前,需验证gem/包是否实际安装——而非仅存在初始化文件。包裹在/
if defined? Foo中的初始化程序在包未捆绑时会静默无操作。if PACKAGE in sys.modules技术栈 检查方式 Ruby/Rails (4空格缩进=顶级gem)grep -E "^ GEM_NAME " Gemfile.lockNode 或grep "\"PACKAGE_NAME\":" package-lock.jsonnode -e "require('PACKAGE_NAME')"Python pip show PACKAGE_NAMEGo 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, Joi error trees) to clients
err.issues -
Baseline header starter values (paste-and-tune):
Header Value Strict-Transport-Security— preload requires verifying everymax-age=63072000; includeSubDomains; preloadserves HTTPS first*.example.comX-Content-Type-OptionsnosniffX-Frame-Options(defence-in-depth alongside CSPDENY)frame-ancestorsReferrer-Policystrict-origin-when-cross-originPermissions-Policy— notecamera=(), microphone=(), geolocation=(), browsing-topics=()is the legacy FLoC name (Chrome ≤ 100); current Chrome usesinterest-cohortbrowsing-topicsContent-Security-Policystart with ; full CSP needs per-site script audit (inlineframe-ancestors 'self', JSON-LD, analytics)<style>HSTS preload submission is sticky — removal takes months. Verify before submitting. -
Where security headers live, by framework:
- Next.js:
next.config.{js,ts}block;headers()vercel.jsonheaders - Rails: ,
config/initializers/secure_headers.rbconfig/application.rb - Express:
app.use(helmet()) - Django: settings in
SECURE_*settings.py
- Next.js:
-
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, etc. inside Next.jsnode:net/ Cloudflare Workers / Vercel Edge functions compile cleanly and fail at first request withmiddleware.ts. Audit middleware and edge-marked routes for Node-only imports; prefer Web Crypto (Failed to load external module) for portable codecrypto.subtle -
Rails admin-engine mounts. Grepfor 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:
config/routes.rbbashgrep -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, which leaves staging, review apps, and preview deploys serving the admin UI anonymously. Fix shape: switch the guard toif Rails.env.production?(Rails 7.1+ helper forunless Rails.env.local?), and add a fail-closed check that refuses access when the auth env vars are unset.development || test -
Concurrent-execution races on paid endpoints. When an endpoint triggers paid downstream work (LLM calls, third-party APIs, scrape jobs), look forfollowed by a
SELECTcall without an intervening atomic claim, and unconditionalrunX()writes. Two concurrent requests can both pass the read-side check and both run. Fix: conditionalUPDATE … SET status='processing', or a Postgres advisory lock. Charge rate-limit budget only on a successful claim so polling and retries don't burn quota.UPDATE … WHERE id = ? AND status = 'pending' RETURNING …
(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.jsrule merging. Rules in
headers()next.config.tsmatch per route and merge — a more-specific rule does not override headers it doesn't redeclare. Shippingheaders()+frame-ancestors 'none'in aX-Frame-Options: DENYdefault plus/:path*in aframe-ancestors *override results in both/embedandframe-ancestors *on the embed route (contradicting; older browsers may break framing). Verify withX-Frame-Options: DENYagainst the deployed origin — config inspection alone misses the merge. Either set XFO in every rule or drop it entirely (CSPcurl -Isupersedes on modern browsers).frame-ancestors -
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 toon every deploy or scheduled invocation, the route-level signature check never runs, and the integration appears to "work" until it doesn't.
/login -
Bearer-token compare with unset env interpolation. Comparisons that interpolatewithout a presence check —
process.env.XBearer ${process.env.WEBHOOK_TOKEN}`\"Bearer undefined"— resolve to a literalif (!process.env.WEBHOOK_TOKEN) throw new Error(...)`.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: -
API routes returning HTML 302 redirects instead of JSON 401. Auth middleware that 302s every unauthenticated request tobreaks
/loginclients (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 returnfetchwith a machine-readable body.401 application/json
-
生产配置中启用调试模式
-
过于宽松的CORS策略()
Access-Control-Allow-Origin: * -
缺少HTTP安全头(CSP、HSTS、X-Frame-Options、X-Content-Type-Options)
-
附带默认凭据或配置
-
详细错误消息暴露堆栈跟踪或内部信息——包括验证库向客户端回显模式细节(如Zod、Joi错误树)
err.issues -
基准安全头起始值(可直接复制后调整):
头信息 值 Strict-Transport-Security——preload需先验证所有max-age=63072000; includeSubDomains; preload都支持HTTPS*.example.comX-Content-Type-OptionsnosniffX-Frame-Options(与CSPDENY配合实现深度防御)frame-ancestorsReferrer-Policystrict-origin-when-cross-originPermissions-Policy——注意camera=(), microphone=(), geolocation=(), browsing-topics=()是旧版FLoC名称(Chrome ≤100);当前Chrome使用interest-cohortbrowsing-topicsContent-Security-Policy从 开始;完整CSP需针对站点脚本进行审计(内联frame-ancestors 'self'、JSON-LD、分析脚本)<style>HSTS预提交是不可逆的——移除需要数月时间。提交前需验证。 -
各框架中安全头的配置位置:
- Next.js:的
next.config.{js,ts}块;headers()的vercel.jsonheaders - Rails:、
config/initializers/secure_headers.rbconfig/application.rb - Express:
app.use(helmet()) - Django:中的
settings.py设置SECURE_*
- Next.js:
-
运行时API不匹配:在Edge/Workers/V8隔离运行时中运行的代码无法加载仅Node可用的模块。Next.js/Cloudflare Workers/Vercel Edge函数中导入
middleware.ts、node:crypto、node:fs、node:buffer等模块会编译成功,但首次请求时会抛出node:net错误。审计中间件和标记为edge的路由是否存在仅Node可用的导入;优先使用Web Crypto(Failed to load external module)实现可移植代码。crypto.subtle -
Rails管理引擎挂载:搜索中的引擎和仪表板挂载(PgHero、Sidekiq::Web、Flipper UI、Mission Control、Audit1984),并验证对应初始化程序中的授权中间件在所有可从互联网访问的环境中都生效——而非仅在生产环境:
config/routes.rbbashgrep -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示例均将授权包裹在中,这会导致 staging、评审应用和预览部署匿名提供管理UI。修复方式:将防护切换为if Rails.env.production?(Rails 7.1+的unless Rails.env.local?辅助函数),并添加失败关闭检查,当授权环境变量未设置时拒绝访问。development || test -
付费端点的并发执行竞争:当端点触发付费下游工作(LLM调用、第三方API、爬虫任务)时,查找后直接调用
SELECT且未进行原子声明的代码,以及无条件执行runX()的代码。两个并发请求可能同时通过读取检查并执行操作。修复方案:条件UPDATE … SET status='processing',或使用Postgres advisory锁。仅在成功声明后才扣除速率限制配额,避免轮询和重试消耗配额。UPDATE … WHERE id = ? AND status = 'pending' RETURNING …
(注:此条目归属于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针对部署源进行验证——仅检查配置会遗漏合并问题。要么在每个规则中设置XFO,要么完全移除它(现代浏览器中CSPcurl -I会取代XFO)。frame-ancestors -
未豁免Bearer/HMAC路由的授权中间件:定时任务、Stripe/GitHub Webhook以及任何通过Bearer令牌或HMAC认证的路由需要从会话Cookie中间件中排除。症状:这些路由在每次部署或定时调用时会静默302重定向到,路由级签名检查从未执行,集成看似“正常”直到出现故障。API路由应返回
/login及机器可读的响应体。401 application/json -
未设置环境变量时的Bearer令牌比较:未进行存在性检查就插值的比较——
process.env.XBearer ${process.env.WEBHOOK_TOKEN}`\"Bearer undefined"——当环境变量缺失时会解析为字面量if (!process.env.WEBHOOK_TOKEN) throw new Error(...)`。。攻击者若猜到环境变量未设置的情况,可重放该字面量字符串。需在模块加载时断言变量存在: -
API路由返回HTML 302重定向而非JSON 401:将所有未认证请求302重定向到的授权中间件会破坏
/login客户端(它们会跟随重定向并加载HTML),模糊监控中的认证状态,并让攻击者通过302 vs 404判断端点是否存在。API路由应返回fetch及机器可读的响应体。401 application/json
A06: Vulnerable Components
A06: Vulnerable Components(易受攻击组件)
- Run (Node),
npm audit(Python), or equivalentpip audit - Check lock files for known vulnerable dependency versions
- Flag dependencies with critical CVEs
- Run alongside
npm audit --omit=devand triage by reachability:npm audit- Runtime-reachable (in ) — must fix
dependencies - Build-time-only (Vite, esbuild via drizzle-kit, postcss) — usually defer
- Dev-only (linters, test libs) — defer
- Runtime-reachable (in
- For the full CVE picture and triage by reachability, invoke . A06 here is a one-line sanity check.
dependency-audit
- 运行(Node)、
npm audit(Python)或等效命令pip audit - 检查锁文件中是否存在已知易受攻击的依赖版本
- 标记存在严重CVE的依赖
- 同时运行和
npm audit --omit=dev,并按可达性分类处理:npm audit- 运行时可达(在中)——必须修复
dependencies - 仅构建时使用(Vite、通过drizzle-kit的esbuild、postcss)——通常可推迟修复
- 仅开发时使用(代码检查工具、测试库)——可推迟修复
- 运行时可达(在
- 如需完整的CVE情况和按可达性分类处理,调用。此处A06为一行 sanity check。
dependency-audit
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. and equality-checked on read). Even with
cookies.set("admin_token", process.env.ADMIN_PASSWORD)andhttpOnly, this is plaintext-credential storage (CWE-522) and lacks rotation/revocation. Replace with an HMAC-signed expiring token verified viasecure/crypto.subtle.verifycrypto.timingSafeEqual - Non-constant-time credential comparison: for passwords, API keys, or signatures leaks length and prefix-match via timing. Use
submitted === expected(Node) orcrypto.timingSafeEqual(Web Crypto)crypto.subtle.verify - Missing rate limiting on login (credential stuffing risk)
- Broken password reset flows
- Rails / Devise — requires
password_length.:validatableinconfig.password_lengthis only enforced when the User model includesconfig/initializers/devise.rbin its:validatabledeclaration. A model withdevise :...(nodevise :database_authenticatable, :registerable, :recoverable, :rememberable) accepts passwords of any length and any email format, regardless of what the initializer says. Grep::validatablein^\s*devise\s+:; flag any line whereapp/models/is absent. Adding it on an existing app validates on create+update but does not retro-invalidate existing weak passwords.:validatable - NextAuth v5 / Auth.js footguns:
- unset silently derives a weak dev value — assert presence at module load:
AUTH_SECRETif (!process.env.AUTH_SECRET) throw ... - Credentials provider has no built-in rate limit — wrap or add upstream limiter; otherwise credential stuffing is trivial
authorize() - 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,这也是明文凭据存储(CWE-522)且缺少轮换/撤销机制。替换为通过secure/crypto.subtle.verify验证的HMAC签名过期令牌。crypto.timingSafeEqual - 非恒定时间凭据比较:用于密码、API密钥或签名比较时,会通过时间泄露长度和前缀匹配信息。使用
submitted === expected(Node)或crypto.timingSafeEqual(Web Crypto)。crypto.subtle.verify - 登录端点缺少速率限制(存在凭据填充风险)
- 密码重置流程存在缺陷
- Rails / Devise——需要
password_length::validatable中的config/initializers/devise.rb仅当User模型在config.password_length声明中包含devise :...时才会生效。若模型声明为:validatable(无devise :database_authenticatable, :registerable, :recoverable, :rememberable),则会接受任意长度和格式的密码,无论初始化程序如何设置。搜索关键词::validatable在^\s*devise\s+:中;标记任何缺少app/models/的行。在现有应用中添加:validatable会在创建+更新时进行验证,但不会追溯使现有弱密码失效。:validatable - NextAuth v5 / Auth.js陷阱:
- 未设置时会静默生成弱开发环境值——需在模块加载时断言变量存在:
AUTH_SECRETif (!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 / 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
clientSecretforever). Fix: before creating a new resource, retrieve the existing one. If status is non-terminal (processing,processing,requires_payment_method) 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.requires_action - 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 (). 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.
WHERE status='draft'
- 用户输入的不安全反序列化
- CI/CD流水线缺少完整性检查
- 无锁文件完整性验证(SRI哈希)
- 无状态检查的外部资源覆盖:任何创建提供商端资源(Stripe支付意向、订阅、Webhook订阅)并将ID存储在DB行中的处理程序,应在覆盖前检查是否存在仍在处理中的先前资源。旧的/令牌可能仍被客户端持有;完成时,Webhook处理程序可能因ID已更改而无法找到该行(资金到账,但DB状态仍为
clientSecret)。修复方案:创建新资源前,检索现有资源。若状态为非终端(processing、processing、requires_payment_method)且参数未更改,则重用现有资源——返回现有clientSecret。仅当先前资源已取消/成功或参数更改时才创建新资源。requires_action - 持久化DB状态前触发外部副作用:当处理程序同时写入DB并触发外部副作用(邮件、收费、Webhook、S3写入)时,外部调用应在DB写入提交后执行。“DB持久化,外部调用重试”的故障模式可恢复;“外部调用完成,DB状态陈旧”的故障模式不可恢复。修复模式:预留后执行。执行条件UPDATE,将行状态从预操作状态切换为后操作状态()。若影响0行,则拒绝请求。若影响1行,则调用提供商接口。提供商调用失败时,行已反映意图——触发警报并重试。额外优势:此方式还能使处理程序具备幂等性。
WHERE status='draft'
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 (, status code) without PII
error.name
- Grep for:
- 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 address, especially with attacker-influenced subject/body, is a phishing vector against your verified sender's deliverability reputation. Defences (pick one):
to:- Don't auto-confirm — replies happen when a human responds
- Require ownership proof before any reply email (verification link / double opt-in)
- Use a separate, plainly-templated sender with subject/body the attacker can't influence
noreply@
- 未记录认证事件(登录、失败、权限变更)
- 敏感数据写入日志(密码、令牌、PII)
- 对可疑模式无告警机制
- 静默错误吞噬:空catch块会隐藏漏洞和攻击信号(速率限制器故障、反序列化错误、认证失败)。
- 搜索关键词:、
catch \{\}、catch (_) \{\}、catch (_e) \{\}、.catch(() => {})、.catch(() => null)try { ... } catch { return null } - 修复方案:至少记录错误类别(、状态码)且不包含PII
error.name
- 搜索关键词:
- 未认证端点从已验证域名向用户提供的地址发送邮件:任何无需认证即可访问的处理程序,若向攻击者提供的地址触发出站邮件(确认邮件、待认领的密码重置邮件、邀请邮件、“已收到你的消息”邮件),且邮件主题/内容受攻击者影响,则会成为针对已验证发件人交付信誉的钓鱼载体。防御措施(选择其一):
to:- 不自动确认——仅当人工回复时才发送邮件
- 在发送回复邮件前要求所有权证明(验证链接/双重选择加入)
- 使用单独的、模板固定的发件人,且主题/内容不受攻击者影响
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: (when only
http://allowed.com/is expected)https: - Embedded credentials:
https://user:pass@allowed.com/ - -host trick:
@(hostname resolves tohttps://allowed.com@evil.com/)evil.com - Non-default ports:
https://allowed.com:8443/ - Punycode/IDN spoof: (Cyrillic а) or
https://аllowed.com/xn--llowed-pdc.com - Trailing dot: (DNS-equivalent, often missed by string compare)
https://allowed.com./ - Subdomain confusion:
https://allowed.com.evil.com/ - Bracketed IPv6 literal:
https://[::1]/ - Bare IPv4:
https://127.0.0.1/ - Decimal-integer IPv4: → 127.0.0.1
http://2130706433/ - Hex IPv4:
http://0x7f000001/ - Octal IPv4: ; zero-padded
http://0177.0.0.1/→ 8.0.0.1 (octal!)0010.0.0.1/ - IPv4-mapped IPv6: → block the whole
http://[::ffff:127.0.0.1]/range::ffff:* - Trailing-dot hostname: ,
http://localhost./http://metadata.google.internal./ - Cloud metadata endpoints: AWS , GCP
169.254.169.254, ECSmetadata.google.internal169.254.170.2 - CGNAT range: –
100.64.0.0100.127.255.255 - Link-local IPv6: ; unique-local IPv6:
fe80::/10fc00::/7
- Wrong scheme:
- Fetch-time guards: (don't follow attacker-controlled redirects), explicit timeout, no following 3xx into the metadata service
redirect: "error" - 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 header, or use a vetted proxy
Host: - Image-optimizer-as-proxy (Next.js, Nuxt, SvelteKit): with
next.config.{ts,js},images.remotePatterns: [{ hostname: '**' }], or any wildcard entry lets attackers route arbitrary URLs through your CPU/bandwidth.domains: ['*']- Grep for: ,
remotePatternsin image configdomains: - Fix: pin to specific known hostnames; leave empty if all images are local.
- Grep for:
- Grep for: ,
fetch(,axios(,http.get(,urllibwith user inputrequests.get(
- 用户可控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 - 末尾点:(DNS等效,但字符串比较常遗漏)
https://allowed.com./ - 子域名混淆:
https://allowed.com.evil.com/ - 带括号的IPv6字面量:
https://[::1]/ - 纯IPv4:
https://127.0.0.1/ - 十进制整数IPv4:→ 127.0.0.1
http://2130706433/ - 十六进制IPv4:
http://0x7f000001/ - 八进制IPv4:;零填充
http://0177.0.0.1/→ 8.0.0.1(八进制!)0010.0.0.1/ - IPv4映射IPv6:→ 阻止整个
http://[::ffff:127.0.0.1]/范围::ffff:* - 末尾点主机名:、
http://localhost./http://metadata.google.internal./ - 云元数据端点:AWS 、GCP
169.254.169.254、ECSmetadata.google.internal169.254.170.2 - CGNAT范围:–
100.64.0.0100.127.255.255 - 链路本地IPv6:;唯一本地IPv6:
fe80::/10fc00::/7
- 错误协议:
- 请求时防护:(不跟随攻击者控制的重定向)、显式超时、不跟随3xx重定向到元数据服务
redirect: "error" - 注意验证与请求之间的TOCTOU问题——DNS在两次操作中可能解析不同(DNS rebinding)。对于高风险调用者,固定解析后的IP并通过IP连接同时设置头,或使用经过验证的代理。
Host: - 图片优化器作为代理(Next.js、Nuxt、SvelteKit):中设置
next.config.{ts,js}、images.remotePatterns: [{ hostname: '**' }]或任何通配符条目,会让攻击者通过你的CPU/带宽路由任意URL。domains: ['*']- 搜索关键词:、
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) build successfully but throw on first request.node:fs - Lazy module loads. Adapters/plugins loaded via or runtime DI surface only when the codepath runs.
import() - Environment-variable fallthrough. with X unset becomes a literal that the tests never hit because the test env defines X.
Bearer ${process.env.X}
For each shipped fix, run the affected route or job and capture the response. + build success ≠ fix verified.
tsc --noEmitFor 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 - 懒加载模块:通过或运行时DI加载的适配器/插件仅在代码路径执行时才会暴露问题。
import() - 环境变量传递问题:在X未设置时会成为字面量,而测试环境中X已定义,因此测试不会覆盖此场景。
Bearer ${process.env.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 ( vs , IPv4-mapped IPv6, status-enumeration via ordered 404/400/401, callback-URL control chars, concurrent-execution races on paid endpoints).
localhost.localhostAfter 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 doesn't help if you call
rateLimitprogrammatically — that bypasses the HTTP router where the limiter attaches.auth.api.signInEmail(...)
单次审计可可靠捕获检查清单中的类别,但会遗漏清单未提及的特定绕过方式( vs 、IPv4映射IPv6、通过有序404/400/401枚举状态、回调URL控制字符、付费端点的并发执行竞争)。
localhost.localhost生成第一份报告并应用修复后,以明确的对抗性框架进行二次评审(“假设作者过于自信;找出他们遗漏的内容”)——理想情况下使用不同模型或代理,打破关联盲点。将与第一次评审的任何分歧视为更有价值的发现。
二次评审中常见的发现:
- 修复本身引入的新攻击面(豁免路由导致的认证绕过、新查询引入的IDOR)
- 重写过程中过时的注释
- 新代码中的边界条件(环境变量未设置传递、空输入)
- 修复与报告之间的文档偏差
- 配置第三方库的修复:当修复是库文档中的代码片段(认证、加密、HTTP客户端、速率限制中间件)时,片段可能正确但仍未在你的代码路径中运行。在声明修复完成前:搜索固定版本的库代码,从你的调用点追踪到配置影响的代码路径。示例:启用Better Auth的对通过
rateLimit编程式调用无帮助——这会绕过速率限制器附加的HTTP路由器。auth.api.signInEmail(...)
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:
- Why the fix doesn't apply — cost tier, dependency version constraint, deployment topology, vendor limitation
- Compensating controls — private network, signed cookies, internal-only routing, etc.
- 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中部署。待验证后关闭。
- 推迟修复——确认修复并已安排时间。指定是否阻止下一次部署(发布阻塞)或不阻止(可接受风险并计划在指定日期修复)。严重性不会因决定推迟而改变。
- 接受风险——当前配置下不计划修复。报告必须记录:
- 修复不适用的原因——成本层级、依赖版本约束、部署拓扑、供应商限制
- 补偿控制措施——私有网络、签名Cookie、仅内部路由等
- 重新评估触发条件——什么条件(套餐升级、依赖版本更新、流量模式变化)会导致此发现退出“接受风险”类别
缺少以上三个字段的“接受风险”条目是被悄悄换标签的真实问题。
每个发现的问题需记录:
markdown
undefined[SEVERITY] A0X: [Title]
[严重性] A0X: [标题]
File:
CWE: CWE-XXX
path/to/file.ts:42Description: [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文件:
CWE: CWE-XXX
path/to/file.ts:42描述: [漏洞是什么以及为何重要]
易受攻击代码:
[代码片段]
修复方案:
[带解释的修复后代码片段]
验证: 具体的对抗性输入 + 证明修复有效的命令或代码路径。对于XSS:不再突破的脚本标签载荷。对于开放重定向:现在被拒绝的外部URL。对于密码长度:现在无法保存的1字符密码。“代码检查工具显示没问题”不是验证——静态分析对于同时也是安全修复的正确性漏洞存在已知盲点。
生成执行摘要:
```markdownSecurity 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
优先修复计划
- [Critical fixes — immediate]
- [High fixes — this week]
- [Medium/Low — scheduled]
undefined- [严重修复——立即处理]
- [高优先级修复——本周内处理]
- [中/低优先级——已安排]
undefinedBoundaries
边界
- 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