发生了什么
根据 SafeState 对研究人员分析的整理,攻击过程大致如下:
- Polymarket 的网站从 DebugBear 的 CDN 加载了一个真实用户监控(RUM)脚本。这是很常见的配置,成千上万的网站都以同样方式接入几十家供应商。
- 攻击者把这个文件替换成了恶意 JavaScript。由于浏览器每次都直接从供应商那里获取最新文件,每位访客都立即拿到了新版本。
- 该脚本会检查是否存在有效的 Polymarket 会话,从浏览器存储中读取账户数据、钱包信息和交易 API 凭证,然后借用 Polymarket 自己的签名流程,把仓位转移或卖出到攻击者的钱包。
值得细想的一点是:研究人员指出,硬件钱包和强密码都没有用,因为这个脚本根本不需要私钥。它就在用户自己的浏览器会话里,以已登录用户的身份行事。
据同一份报告,Polymarket 的修复方式是一次移除该供应商脚本的生产环境提交,交易在当天下午就恢复了。SafeState 还指出,这并不是该平台今年遭遇的第一起供应商脚本事件。
为什么这与你的应用有关
一个指向 cdn.some-vendor.com 的 script 标签,拥有与你自己代码相同的权力。它可以读取 DOM、读取 localStorage 和 sessionStorage、带着用户凭证调用你的 API,还能改变页面显示的内容。在“你的代码”和“你在上线那周随手粘贴进来的分析代码片段”之间,没有任何沙箱。
这对快速交付的应用和 AI 构建的应用尤其重要,因为:
- 脚本越积越多。每一句“加个分析”“加个聊天气泡”“加个错误追踪”的提示词,往往都会再添一个标签,事后很少有人删掉它们。
- 令牌存放在浏览器存储里。很多入门模板把会话或 API 令牌放在 localStorage 中,页面上的任何脚本都能读到。
- 通常没有设置 Content-Security-Policy。没有它,任何被注入或替换的脚本都可以从任意位置加载更多代码,并把数据发往任意地方。
你没法审计供应商的构建流水线,但你可以限制一个坏脚本能造成多大破坏。
如何检查你的应用
1. 列出页面加载的每一个脚本。打开你的应用,进入 DevTools 的 Network 面板,按 JS 过滤,然后刷新页面。或者在每个重要页面的控制台里运行:
[...document.scripts].map(s => s.src || '(inline)').filter(Boolean)把所有不属于你的域名记下来。登录后的页面也要检查,而不仅仅是营销网站。标签管理器可能会加载更多脚本,而这些脚本不会出现在你的 HTML 源码里。
2. 检查这些标签中哪些带有 integrity 属性。在控制台中运行:
[...document.querySelectorAll('script[src]')]
.filter(s => !s.src.startsWith(location.origin))
.map(s => ({ src: s.src, sri: !!s.integrity }))3. 查看响应头。运行 curl -sI https://yourapp.com,找 Content-Security-Policy。如果没有,或者其中包含 script-src * 或不带 nonce 的 'unsafe-inline',那它基本起不到什么限制作用。
4. 检查会话存放在哪里。在 DevTools 的 Application 面板下查看 Local Storage 和 Session Storage。如果你在里面看到访问令牌、刷新令牌或 API 密钥,页面上的任何脚本都能读取它们,而这正是 Polymarket 恶意载荷所做的事。
如何修复
删掉不需要的脚本。这是收益最高的一步。SafeState 给运营方的建议,第一条就是维护第三方脚本清单,并把非必要的标签从账户、结账和交易页面中移除。你的登录、账单和设置页面很少需要热力图工具。
用 Subresource Integrity(SRI)固定并校验你保留的脚本。如果供应商提供带版本号的文件,就引用某个具体版本并加上哈希。一旦文件发生变化,浏览器会拒绝执行它:
<script
src="https://cdn.example-vendor.com/sdk/4.2.1/sdk.min.js"
integrity="sha384-REPLACE_WITH_REAL_HASH"
crossorigin="anonymous"></script>根据你审查过的那个确切文件,自己生成哈希:
curl -s https://cdn.example-vendor.com/sdk/4.2.1/sdk.min.js \
| openssl dgst -sha384 -binary | openssl base64 -A要注意其中的取舍:如果供应商在不带版本号的 URL 上原地更新脚本(监控和分析代码片段很常见这样做),SRI 就会让它失效。对于这类脚本,要么自托管一份固定版本、由你有意识地更新,要么接受你在信任供应商的线上文件,并让它远离敏感页面。
添加 Content-Security-Policy。CSP 告诉浏览器哪些来源可以提供脚本,以及页面可以把数据发往哪些来源。一个 Next.js 或 Express 应用的起点可以是这样:
Content-Security-Policy:
default-src 'self';
script-src 'self' https://cdn.example-vendor.com;
connect-src 'self' https://api.yourapp.com;
frame-ancestors 'none';
base-uri 'self';
object-src 'none'对 CSP 在这里能起的作用要有清醒认识:如果供应商自己的域名在白名单里,CSP 拦不住从该域名提供的恶意文件。CSP 能做的是阻止这个脚本从新的域名拉取代码,或把窃取的数据发送到不在 connect-src 中的任意端点。先用 Content-Security-Policy-Report-Only 上线,看看哪些东西会出问题。我们关于添加 Content-Security-Policy 的指南会一步步带你完成。
让会话令牌脱离 JavaScript 的触及范围。把会话令牌移到带有 HttpOnly、Secure 和 SameSite 标记的 Cookie 中,让页面脚本无法读取。这挡不住恶意脚本在页面打开期间以用户身份发起请求,但能防止令牌被窃取后在别处重复使用。参见 Secure、HttpOnly 和 SameSite Cookie 标记。
保持你自己的前端密钥干净。如果一个脚本能读取你的打包文件,它就能读到你打包进去的任何机密,而机器人早已在搜寻泄露的密钥和 .env 文件。看看我们关于客户端 JavaScript 中暴露的 API 密钥的指南,确认你没有在生产环境中发布 source map;如果你已经受过一次惊吓,我们关于在不停机的情况下轮换应用机密的文章会告诉你如何善后。
要点清单
在关掉 DevTools 之前,请确认你已经:
- 列出公开页面和登录后页面上的每一个第三方脚本。
- 删除不再使用的脚本,尤其是登录、账单和账户页面上的。
- 在供应商支持的地方固定版本并添加 integrity 哈希。
- 无法固定版本的脚本,要么自托管,要么让它远离敏感页面。
- 部署 Content-Security-Policy,并收紧 script-src 和 connect-src。
- 把会话令牌放在 HttpOnly Cookie 中,而不是 localStorage。
- 每当有人(或某个 AI 代理)添加新标签时,重新做一遍这次审计。
看看你的现状
如果你想快速建立基线,AppSafe 会检查你的安全响应头,包括是否设置了 Content-Security-Policy;它的已验证完整扫描还会检查 Subresource Integrity 和存在漏洞的 JavaScript 库等前端问题。你可以在示例报告中看看报告长什么样,或者搭配我们的 AI 构建应用上线前检查清单一起使用。
Polymarket 在暂停交易后约一小时内就查明了原因。很多小团队要过更久才会发现某个脚本被改动了。今天就为你的应用运行一次免费的 AppSafe 扫描,看看你的现状。
