一键导入
client-js-audit
客户端 JS 安全白盒审计——覆盖 DOM XSS(客户端 source 流到客户端 sink)、Token 与凭据客户端 存储泄露、postMessage 跨源通信、客户端安全决策(鉴权 / 加解密 / feature flag)四个维度。
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
客户端 JS 安全白盒审计——覆盖 DOM XSS(客户端 source 流到客户端 sink)、Token 与凭据客户端 存储泄露、postMessage 跨源通信、客户端安全决策(鉴权 / 加解密 / feature flag)四个维度。
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
白盒建模——一次深度建模产出五张共享模型:项目架构(技术栈/分层/框架自有封装/闭源依赖位置)、 入口点(路由/中间件信任边界)、认证与权限(会话/角色/多租户/归属字段)、业务(实体/流程状态机/ 业务不变量/高价值资产)、全局威胁(攻击面×漏洞类别的适用性映射,供下游逐一全测)。本能力不做漏洞判定,为后续 漏洞维度分析建立共享底图。
OS 命令注入综合检测——覆盖直接命令拼接 / shell 元字符 / 参数注入 / 间接 RCE,按回显 / 时间 / 带外通道分诊。 流量中参数值含主机名 / URL / 文件名 / 命令片段、响应里出现 shell 错误关键字(`/bin/sh` / `cmd.exe` / `command not found`)、注入 `sleep N` 耗时增加、上传后异步处理链路时使用。
文件上传多策略综合检测——覆盖扩展名 / MIME / 魔术数 / 解析漏洞 / 上传链组合(LFI / Zip Slip / SVG XSS / SVG XXE)。 流量含 multipart/form-data 上传、响应回显落盘路径、上传后文件可被 GET 解析、上传字段含 filename / path 可控时使用。
IDOR 水平越权检测 — 通过替换资源标识符(ID/UUID/路径)访问他人资源的风险;适用于用户资料、订单、文档、租户隔离场景。
检测路径穿越和本地文件包含(LFI)风险;当目标存在文件读取/下载/预览功能且含路径参数时触发;适用于文件下载、日志查看、模板加载等场景。
黑盒建模 — 迭代式攻击面建模,按站点级 / 页面级 / 功能级三粒度推进;进入新页面即触发一次页面级建模、切换新身份各刷新一次,产出端点账本与页面语义模型,为后续漏洞维度的适用性判断提供依据。
| name | client-js-audit |
| description | 客户端 JS 安全白盒审计——覆盖 DOM XSS(客户端 source 流到客户端 sink)、Token 与凭据客户端 存储泄露、postMessage 跨源通信、客户端安全决策(鉴权 / 加解密 / feature flag)四个维度。 |
| when-to-use | 当项目前端存在安全敏感 JS 逻辑(token 存储/DOM 操作/postMessage/eval/innerHTML)时 |
| allowed-tools | bash,read_file,list_files,rg |
| user-invocable | true |
按 代码 pattern + 框架信号 + 浏览器 API 使用三维分类。
代码 pattern 维度(grep 命中模式):
innerHTML / outerHTML / document.write / insertAdjacentHTMLeval( / new Function( / setTimeout(stringArg) / setInterval(stringArg)location.hash / location.search / document.URL / document.referrer / window.name / URLSearchParamswindow.addEventListener('message' / window.postMessage( / iframe.contentWindow.postMessage(localStorage.setItem / localStorage.getItem / sessionStorage.* / document.cookieif (user.role === / if (isAdmin) / if (permissions.includes( 等出现在 *.vue / *.tsx / *.jsx 的视图层框架信号维度:
*.vue 含 v-html / v-bind:href / 模板编译器配置*.tsx / *.jsx 含 dangerouslySetInnerHTML / href={...}*.component.ts 含 [innerHTML] / bypassSecurityTrustHtml*.svelte 含 {@html ...}*.js 含 $(...).html( / $(...).append( / $.parseHTMLrouter/index.js(Vue Router)/ App.tsx 的 React Router 配置浏览器 API 使用维度:
window.open / postMessage handlernavigator.serviceWorker.register / new Worker(new WebSocket( 接收消息后写入 DOMchrome.runtime / browser.runtime)业务命名(如方法名 renderProfile)只作粗筛——sink 语义相同就是审计候选。与 stored-xss-detection 的分流口径:本能力 source 是「客户端可控」(URL / hash / postMessage / 浏览器 API),持久化 source(DB → API → 渲染)走 stored-xss-detection,sink 集合可能重叠但 source 维度独立计数。
客户端 JS 安全的核心成因是客户端可控 source 未经 sanitize 流向客户端 sink,加之浏览器同源策略与本地存储的设计假设被违背:
document.location / document.URL / document.referrer / window.name / URL fragment / postMessage event.data 等客户端可控 source 流入 innerHTML / eval / Function() / setTimeout(string) 等 sink,攻击者构造的字符串改变了 DOM 结构或被求值执行。这类漏洞数据流完全在浏览器内,服务端日志看不到。event.origin → 任意页面(含 evil.com 嵌入的 iframe)都能向受害页发消息触发其处理逻辑;发送端用 targetOrigin = '*' → 任意拦截到该 iframe 的页面都能读到消息内容。客户端 JS 安全的 source / sink 都在浏览器侧,数据流追踪要注意打包工具(webpack / vite / rollup)会重写代码——必要时看打包前源码而非 dist/ 产物。
客户端层 source 集合:
location.hash / location.search / location.pathname / location.href / URLSearchParams 解析结果document.URL / document.documentURI / document.referrer / document.baseURI / window.namepostMessage event.data / event.origin(origin 本身可控,参 §10 反例)/ event.sourcews.onmessage event.data / es.onmessage event.datalocalStorage.getItem / sessionStorage.getItem / document.cookieelement.getAttribute('data-*') 当属性值由攻击者注入时chrome.runtime.onMessage / window.addEventListener('message') from content script客户端层 sink 集合:
Element.innerHTML / Element.outerHTML / document.write / document.writeln / Element.insertAdjacentHTMLeval(string) / new Function(string) / setTimeout(string) / setInterval(string)Element.setAttribute('on*', ...) / element.onclick = stringHandler / href = "javascript:..." / src = userControlledURL$(el).html(...) / $.parseHTML(...) / $(el).append(htmlString) / $(el).after(htmlString) / $(el).prepend(htmlString)v-html / React dangerouslySetInnerHTML / Angular [innerHTML] 配合 bypassSecurityTrustHtml / Svelte {@html ...}{{{triple}}} / Mustache 无转义块 / Underscore _.template 配合 unescapejavascript: 协议时可执行):location.href = ... / location.assign(...) / window.open(...) / <a href={userInput}>数据流追踪规则:
dist/*.js 已混淆,必须看源码仓库(src/);source map 缺失时降级 §11{{ }} 自动 HTML 转义、React {value} 自动转义、Angular DomSanitizer——只对正确通道生效,绕过通道(v-html / dangerouslySetInnerHTML / bypassSecurityTrust*)失效| 类型 | 静态识别特征 | 白盒识别难点 |
|---|---|---|
| DOM XSS(hash / search) | location.hash / location.search → innerHTML / eval | 现代框架常用 hash 路由,需区分路由匹配(安全)和 hash 内容直接渲染(危险) |
| postMessage 越权 | addEventListener('message', handler) handler 无 event.origin 校验 | origin 校验 typo(includes / 前后缀绕过)易漏检 |
| 客户端鉴权 | 视图层 if (user.role === 'admin') 控制 UI 显隐 | 必须交叉验证后端是否有独立鉴权——白盒只能标 suspected,需配合后端审计 |
| localStorage Token 泄露 | localStorage.setItem('token', ...) / localStorage.setItem('jwt', ...) | 部分项目用 localStorage 缓存非敏感数据,需按键名语义判断 |
| 前端硬编码密钥 | JS 字面量含 apiKey / secret / 私钥 PEM 块 | 部分公钥(如 reCAPTCHA site key)合法暴露,需按算法语义判断 |
eval(JSON.parse 备胎) | try { JSON.parse(x) } catch { eval(x) } 或老代码直接 eval(jsonString) | 旧 IE 兼容代码残留 |
jQuery $() 选择器解析 HTML | $(userInput) 当 userInput 以 < 开头时被当 HTML 解析 | jQuery < 3.5.0 行为;现代项目可能升级后规则不再适用 |
| Vue / React 模板编译注入 | Vue 编译期模板字符串含用户输入;React React.createElement 动态 type | 编译期注入只在运行时 compile 模式(如 Vue runtime-compiler 而非 runtime-only)触发 |
href="javascript:..." 协议 | <a href={userInput}> / v-bind:href="userInput" 缺协议白名单 | 框架不拦截 javascript: 协议,需手动校验 |
| postMessage handler 进 eval / innerHTML | message handler 内调 eval(event.data) 或 el.innerHTML = event.data | 即使 origin 校验对,handler 内部仍可能把 data 当代码执行 |
按项目结构找客户端 JS 安全审计的 source / sink 候选位置:
下列框架 / 项目类型仅作类似项目示例,不限于此;以目标实际栈为准。
| 项目类型 | source / sink 高密度位置 |
|---|---|
| Vue / Nuxt | src/views/*.vue / src/pages/*.vue / src/components/*.vue(找 v-html / v-bind:href / this.$route.hash);src/router/index.js(hash mode / beforeEach 守卫);src/utils/dom.js / src/utils/storage.js(DOM / Token 封装);.env.* 里 VITE_* / VUE_APP_* 前缀变量会进 bundle |
| React / Next.js / Remix | src/pages/*.tsx / app/**/page.tsx(dangerouslySetInnerHTML / <a href={...}>);src/components/*.tsx(useSearchParams / useLocation 流向 DOM);src/stores/*.ts(Zustand / Redux Token 存取);NEXT_PUBLIC_* / REACT_APP_* 前缀变量进 client bundle |
| Angular | src/app/**/*.component.{ts,html}([innerHTML] / bypassSecurityTrustHtml);*.service.ts(Token / HTTP interceptor);*.guard.ts(路由守卫鉴权判定) |
| jQuery / 传统多页 | public/*.html 内嵌 <script>;assets/js/*.js($(...).html( / $(...).append( / 老 eval / setTimeout(string)) |
| postMessage / 扩展 | grep addEventListener('message' 找 handler;grep <iframe 找嵌入点核查 src 可控性;浏览器扩展 manifest.json 查 content_scripts / permissions |
通用建议:优先看源码仓库(src/),不是 dist/ 打包产物——dist 混淆 + 没 source map 会让追源-sink 极难;若只有 dist 走 §11。与 secret-detection 协作扫前端硬编码密钥,与 csp-audit 协作评估 CSP 兜底(CSP 严格度直接影响 DOM XSS 利用难度)。
| 框架 | 安全形态 | 危险形态 |
|---|---|---|
| Vue 2 / Vue 3 | {{ value }} 自动 HTML 转义;v-bind:text-content | v-html="value";v-bind:href="value"(缺协议白名单) |
| React | {value} 自动转义;<div>{userInput}</div> | dangerouslySetInnerHTML={{__html: value}};<a href={userInput}> |
| Angular | {{value}} / [innerText]="value";DomSanitizer 默认 | [innerHTML]="value" + DomSanitizer.bypassSecurityTrustHtml(value) |
| Svelte | {value} 自动转义 | {@html value} |
| jQuery | $(el).text(value);$(el).attr('href', sanitizedUrl) | $(el).html(value);$.parseHTML(value);$(value) 当 value 以 < 开头 |
| 原生 DOM | element.textContent = value;element.setAttribute('data-x', value) | element.innerHTML = value;element.setAttribute('onclick', value) |
| postMessage handler | 第一行 if (event.origin !== EXPECTED_ORIGIN) return; + schema 校验 + 不进 sink | 无 origin 校验;origin 校验用 includes / endsWith 可绕过;handler 内 eval(event.data) |
| postMessage 发送端 | target.postMessage(data, 'https://trusted.example.com') | target.postMessage(data, '*') |
| Token 存储 | HttpOnly + Secure + SameSite=Strict Cookie;后端管理 session | localStorage.setItem('token', jwt);sessionStorage.setItem('jwt', ...) |
| 客户端路由鉴权 | UI 层仅做显隐;后端每个 API 独立鉴权 | 仅前端路由守卫拦截,后端无对应鉴权 |
| 客户端加解密 | 仅做展示混淆 / 反爬,不当真正鉴权 | 把加签 / 加密当作"安全机制"且密钥在 bundle 里 |
框架 sanitize 绕过点(常见误判位):
bypassSecurityTrustHtml / bypassSecurityTrustUrl / bypassSecurityTrustResourceUrl 都是显式跳过 sanitize 的"逃生口"javascript: 协议,需手动 if (!url.startsWith('http')) 类校验加载本 skill 时按这些问题思考:
event.origin?校验是否是 === 严格相等,而不是 includes / endsWith(易绕过)?是否还校验 event.source?.env.* 的 VITE_* / NEXT_PUBLIC_* / VUE_APP_* 前缀变量)?project-framework-analysis 输出,识别前端框架(Vue / React / Angular / Svelte / jQuery / 原生)、打包工具(webpack / vite / rollup)、客户端路由模式(hash / history)package.json 依赖里的高风险库(jQuery < 3.5.0、Vue 2.6 模板编译器、DOMPurify 是否存在等)# DOM 直写 + 动态求值 sink
rg "\.innerHTML\s*=|\.outerHTML\s*=|document\.write\(|insertAdjacentHTML\(|\beval\(|new Function\(|setTimeout\([\"\\'`]|setInterval\([\"\\'`]"
# 框架特定 sink + jQuery sink
rg "v-html|dangerouslySetInnerHTML|\[innerHTML\]|bypassSecurityTrust|\{@html |\\\$\\([^)]+\\)\\.html\\(|\\\$\\.parseHTML\\("
# postMessage handler + 发送
rg "addEventListener\\(\\s*['\\\"]message['\\\"]|onmessage\\s*=|\\.postMessage\\("
# 客户端存储
rg "localStorage\\.setItem|sessionStorage\\.setItem|document\\.cookie\\s*="
# 客户端可控 source
rg "location\\.hash|location\\.search|document\\.referrer|window\\.name|URLSearchParams"
对 §1 命中的 sink,追上游:
useRouter().query / this.$route.hash?工具加速:调用 dataflow-analysis MCP 工具做跨函数数据流追踪。
每个 addEventListener('message', handler) 都过这三关:
if (event.origin !== EXPECTED) return;?
=== 还是 includes / endsWith(易绕过,参 §10 反例 5)?.includes() 还是 === 逐个比较?event.source === expectedWindow?(防止同 origin 内多个 iframe 互相发消息伪装)发送端检查:target.postMessage(data, targetOrigin) 的 targetOrigin 是否是具体 URL 而非 '*'?
rg "(localStorage|sessionStorage)\\.setItem" 后按键名语义判断:是否含 token / jwt / auth / credential 语义(漏洞候选),还是单纯 UI 缓存(如 theme / lang,安全)。若是 Token:是否同时有 HttpOnly Cookie 通道(双轨可疑——若 localStorage 是冗余,应删;若是主通道,则是漏洞)。
# 视图层鉴权判定
rg "user\\.role|isAdmin|hasPermission|permissions\\.includes" --type vue --type tsx --type jsx
# 路由守卫
rg "beforeEach|canActivate|requireAuth"
对每个命中:
suspected,需配合后端审计或黑盒访问)与 secret-detection 协作。重点扫:
src/config/*.js / src/constants/*.ts.env* 文件里 VITE_* / NEXT_PUBLIC_* / VUE_APP_* / REACT_APP_* 前缀变量apiKey / secret / privateKey / -----BEGIN以下是已知的检查角度,作为基线起点而非必检硬清单。结合目标代码动态调整,按三态标注(
[x]/[-]/[+])处置。
DOM XSS 维度:
innerHTML / outerHTML / document.write / insertAdjacentHTML)+ 动态求值 sink(eval / new Function / setTimeout(string) / setInterval(string))命中已倒推 sourcev-html / dangerouslySetInnerHTML / [innerHTML] + bypassSecurityTrust* / {@html})已判断 sanitizelocation.hash / location.search / document.referrer / window.name)所有使用点已扫href / src 属性接收用户输入时有 javascript: 协议白名单$(userInput) 解析 HTML 行为)postMessage 维度:
addEventListener('message', ...) handler 都校验 event.origin,且用严格 ===(非 includes / endsWith)event.source 或至少校验 event.data schemapostMessage(data, target) 发送端 target 不是 '*'Token / 凭据维度:
window.* 全局变量客户端决策维度:
suspected 待后端审计)闭环判定 / 取证完整性 / 破坏性动作以 closure-verification.md 为准,下面只列本能力特有的判定上限与产物契约。
为什么这里是「必须」:本节属交付契约——产物结构关系到下游
dataflow-analysis/ 单漏洞 skill /result-with-file机器消费;产物聚合或省略会让整条链路失效,因此是刚性要求。
本能力作为白盒原子能力,判定上限为 static-confirmed(客户端 source 到客户端 sink 数据流可达且中间无 sanitize / 无 origin 校验 / 无白名单),不等于动态 confirmed。升级到 confirmed 必须靠浏览器实际触发(注入 payload 触发 DOM 修改 / 触发 alert / 通过 origin 伪造发消息成功执行)。
static-confirmed(白盒上限,落 status=needs_review):
innerHTML / eval / v-html / dangerouslySetInnerHTML / [innerHTML] + bypassSecurityTrust* / {@html} 等)static-unknown(落 status=needs_review + 标注 unknown):参 §11,构建产物未含 source map / 第三方 SDK(混淆)/ 运行时动态加载的脚本等情形
not_vulnerable(落 status=not_vulnerable):
升级路径(白盒不能独立给 confirmed):
localStorage.getItem('token'),看是否真读到 Token禁止白盒独立判 confirmed——无可观测效果证据,仅静态可达不构成动态利用。
为什么这里是「必须」:产物结构是下游机器消费的接口,聚合 / 省略会让 result-with-file 计数闸门失效,并让单漏洞 skill 无法回溯到具体 file:line。
每确认一条候选立即 append 一行到 shared/coverage-ledger/findings/client-js-audit.jsonl:
{
"id": "client-js-001",
"title": "DOM XSS: location.hash 流入 innerHTML 无 sanitize",
"severity": "high",
"cwe": "CWE-79",
"source": "location.hash",
"sink": "Element.innerHTML",
"file_location": "src/views/Profile.vue:42",
"status": "needs_review",
"confidence": "static-confirmed",
"source_report": "client-js-audit",
"description": "..."
}
字段约束:
id 带 client-js- 前缀全局唯一status ∈ confirmed | needs_review | not_vulnerable | false_positive | superseded(白盒上限默认 needs_review)confidence ∈ static-confirmed | static-unknown(file, sink_type, source_type) 三元组任一不同即各自独立成行——禁止合并折叠(例:同一文件 Profile.vue 里既有 innerHTML 又有 v-html 是 2 行;location.hash → innerHTML 与 postMessage → innerHTML 是 2 行)file_location 填 file:line,不留空、不写区间source_type / sink_type 字段区分(如 source_type=location_hash / sink_type=v_html)禁止:
dist/ 而不看 src/ 就宣称"已完整审计"写"未发现客户端 JS 漏洞"或"已防护"前,产物必须包含:
suspected 待后端审计static-unknown 单元格的具体原因(source map 缺失 / 第三方 SDK / 动态脚本 / Web Worker 等)清单不完整 → 结论降级为 partial-coverage。
反例 1:innerHTML 接收的是 DOMPurify 输出
innerHTML 是预期安全用法el.innerHTML = DOMPurify.sanitize(userInput)ALLOWED_TAGS 包含 <script> 等极宽配置反例 2:React dangerouslySetInnerHTML 来自 Markdown 渲染器
<div dangerouslySetInnerHTML={{__html: marked(content, {sanitize: true})}} />sanitize 选项,改用 DOMPurify 链式调用);确认实际启用反例 3:postMessage handler 严格 origin 校验
if (event.origin !== TRUSTED) return; 后续逻辑安全if (event.origin !== 'https://trusted.example.com') return; processData(event.data);===、白名单是字面量字符串.com / trusted 这类前缀匹配反例 4:localStorage 存非敏感 cache
localStorage.setItem('theme', 'dark') / localStorage.setItem('lang', 'zh-CN')反例 5:postMessage origin 校验 typo(partial match)
includes / endsWith / 正则 partial match 校验 origin 都可绕过if (!event.origin.includes('good.com')) return;——evil-good.com 也命中;if (!event.origin.endsWith('good.com')) return;——evilgood.com 也命中includes / endsWith / match 而不是 ===evil-good.com / goodAcom 类 origin 测试反例 6:Vue v-bind:href="userInput" 缺 javascript: 协议拦截
javascript: 协议<a v-bind:href="userInput"> / <a href={userInput}>,userInput 是 javascript:alert(1) 时点击触发userInput.startsWith('http'))v-bind:href / href={ 看上游是否有协议校验反例 7:Vue 模板编译期 v-html 用户控制
new Vue({template: userInput}) 或 Vue.compile(userInput)vue.runtime.esm.js vs vue.esm.js 引入;grep template: 后接变量的位置反例 8:Cookie Token 缺 HttpOnly / Secure / SameSite
Set-Cookie: token=xxx(无 HttpOnly)或前端 document.cookie = 'token=xxx'HttpOnly / Secure / SameSite=Strict|Laxdocument.cookie 看是否读 Token反例 9:客户端鉴权背后无后端鉴权
v-if="user.role === 'admin'" 隐藏,但 /api/admin/users 端点后端无鉴权suspected,需黑盒访问对应 API 端点确认反例 10:构建产物(dist/)扫不到真洞
innerHTML 在 dist/main.abc123.js 里漏掉但 src/views/Profile.vue 里有白盒底线:不假装看到看不到的代码。本能力的可观测能力到源码 + AST 模式匹配为止。CSP 不评估(属 csp-audit)。
下面这些情形数据流分析无法继续追踪,必须标 static-unknown,不允许默认为 not_vulnerable:
构建产物未含 source map
dist/*.js 混淆代码,无 source map,原始变量名 / 函数名不可恢复static-unknown 记录混淆来源;尝试启用 source map 重新构建源码仓库;不行则结论降级 partial-coverage第三方 SDK(混淆 / 闭源)
analytics-sdk.min.js / payment-widget.js / 三方聊天 widget——闭源混淆static-unknown 记录 SDK 名 + 引入位置;推 dependency-decompile 或直接询问 SDK 提供方运行时动态加载脚本
document.createElement('script'); s.src = ...import(dynamicURL) ESM 动态导入static-unknown 记录加载点;若加载源固定可读,单独审计被加载脚本Web Worker / Service Worker 上下文
static-unknown 记录 Worker 文件;按 Worker 上下文独立审计iframe 跨 origin 调用
static-unknown 记录 iframe src;按对方域名判断是否需要扩大审计范围浏览器扩展注入脚本上下文
static-unknown 记录注入规则;按目标页独立审计运行时配置 / feature flag 驱动
打包工具的 tree-shaking / 代码分割
CSP 边界:本能力不评估 CSP——CSP 严格度影响 DOM XSS 利用难度但不消除漏洞。CSP 评估走 csp-audit。
底线:本能力写"该子系统无客户端 JS 漏洞"前,所有 static-unknown 单元格必须显式列出原因。否则结论降级为 partial-coverage。
textContent 替代 innerHTML;框架自带 {{ }} / {value} 自动转义;需富文本走 DOMPurify.sanitize(最新版本)后再进 sinkhref / src 属性:协议白名单(if (!/^https?:/.test(url)) return;),拒绝 javascript: / data: 协议if (event.origin !== EXPECTED_ORIGIN) return;event.source === expectedWindowtarget.postMessage(data, 'https://trusted.example.com'),禁止 '*'unsafe-inline / unsafe-eval,配 nonce / hash)