本节摘要:Ajax 把浏览器与服务器之间的数据通道打开得更大,攻击面也随之扩大。本节攻两大主题:跨域——同源策略在防什么、CORS 预检的完整往返过程、服务端该怎么配;注入与伪造——XSS 如何借数据流进入页面、输出编码与白名单净化怎么设防、CSRF 为什么能借你的 Cookie 干坏事、Token 与 SameSite 如何堵住它。
现场一(跨域误会):本地开发时页面跑在 5173 端口、接口在 8000 端口,控制台一片红:跨域请求被同源策略拦截。新手的第一反应是"浏览器多管闲事",想找开关关掉它。这个反应方向完全错了——同源策略不是障碍,是你正受其保护的证据。
现场二(XSS 注入):评论区有用户提交了内容为一个图片标签加报错事件属性的留言。列表页用 innerHTML 直接渲染所有评论——每个浏览评论的用户,会话凭证就被这段脚本发走了。存储型 XSS 的完整链条:恶意输入 → 存进数据库 → 借 Ajax 响应回到页面 → 以页面身份执行。
理解这两个现场需要的背景知识是同一块:浏览器给页面划的安全边界,以及Ajax 数据流如何与这条边界交互。

同源的定义三件套:协议、域名、端口全相同。https与http 不同源,a站与b站 不同源,8000与8001 端口不同源。同源策略规定:一个源里的脚本,默认不能读取另一个源的响应。这条策略防的是现场一背后真正的威胁:你登录了银行网站,此时访问了恶意页面,那个页面的脚本向银行接口发请求——没有同源策略,浏览器会自动带上你的银行 Cookie,响应也能被恶意脚本读到,你的余额与转账能力就归它了。
CORS(跨域资源共享)是这条铁律的受控开口:由服务端通过响应头声明"我信任哪个源"。关键认知有三条。
**其一,拦的是读不是发。**跨域请求实际发出去了、服务器也可能处理了,浏览器只是把响应藏起来不给脚本。这解释了一个经典困惑:"日志里怎么有这个跨域请求,前端不是说被拦了吗"——被拦的是读取,不是到达。
其二,预检是先问后做。"简单请求"(GET、POST 表单等少数方法加安全头)直接发,响应里带允许源即可;带自定义头(Authorization)、非简单方法(PUT、DELETE)、或 JSON 体的请求会先触发 OPTIONS 预检——浏览器先问"这个源、这些方法与头,行不行",服务端答"行"才发正式请求。预检结果可以缓存(服务端声明缓存时长),别让它每次都来一轮。
其三,凭证是另一档信任。默认跨域不带 Cookie。要带就必须双端声明:前端 credentials: 'include'(Fetch)或 withCredentials = true(XHR),服务端回显具体源并声明允许凭证——通配符星号与凭证互斥。服务端配置示例:
// 服务端 CORS 配置示意 const allowedOrigins = ['https://app.example.com', 'https://admin.example.com']; app.use((req, res, next) => { const origin = req.headers.origin; if (allowedOrigins.includes(origin)) { res.setHeader('Access-Control-Allow-Origin', origin); // 回显具体源 res.setHeader('Access-Control-Allow-Credentials', 'true'); // 允许凭证 res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type,Authorization'); res.setHeader('Access-Control-Max-Age', '600'); // 预检缓存十分钟 } if (req.method === 'OPTIONS') return res.sendStatus(204); // 预检快速放行 next(); });
⚠️ 生产环境两个绝对禁止:通配符源加允许凭证同开(等于向所有网站开放带 Cookie 的访问,配置形同虚设);把 Origin 原样回显不做白名单校验(等于"谁来都欢迎")。排障提示:报"读了不存在的响应头"时,检查服务端是否声明了 Access-Control-Expose-Headers(2.2 节的坑)。
XSS(跨站脚本)的原理一句话:攻击者让他的数据被你的页面当代码执行。Ajax 数据流是重灾区,因为接口数据最终要进 DOM。
三个注入入口对应三种堵法。
入口一:innerHTML 拼接。3.3 节的事故现场。堵法是默认 textContent——内容当纯文本显示,标签原样可见不执行。需要富文本时用白名单净化(只放行安全标签与属性),不用黑名单(列不全)。
**入口二:属性注入。**把用户数据拼进 HTML 属性(title、自定义属性)时,引号逃逸可以闭合原属性、注入事件属性。堵法是用 DOM API 的 setAttribute——浏览器负责属性的转义边界,不要手工拼属性字符串。
**入口三:URL 注入。**链接的 href 来自用户数据时,javascript: 伪协议是脚本载体。堵法是校验协议白名单(只允许 http、https、mailto 等),或干脆不渲染用户提供的链接。
纵深防御的最后两层。其一 CSP 响应头(内容安全策略):声明"本页面只允许从这些源加载脚本、禁止内联脚本"——即使注入成功,脚本也加载不了。它是"假设前面的防线都破了"之后的保险丝:
Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'
其二 HttpOnly Cookie:会话 Cookie 加上此标记后 JavaScript 完全读不到——就算脚本注入成功,偷凭证这条路也断了。配置在服务端 Set-Cookie 上,是一行配置换一层保险的划算买卖。
判断你的渲染层是否安全,一个快速自查:全库搜索 innerHTML、insertAdjacentHTML、document.write 与拼属性,逐处确认内容来源是可信的或净化过的。3.3 节的策略表(textContent 默认、模板加净化处理富文本)就是执行层。
XSS 是在你的页面里执行代码,CSRF(跨站请求伪造)更阴险——一行你的代码都不用执行。
攻击链条:你登录了银行站,会话 Cookie 还在浏览器里;你顺手点了个"中奖"页面;那个页面里有一个自动提交的表单,目标直指银行的转账接口。浏览器发这个请求时自动带上了银行站的 Cookie——在银行看来,这是"已登录的你"亲手转的账。
Ajax 时代 CSRF 的面有了变化:接口若改用 Authorization 头携带 Token(不自动附带),经典 CSRF 大部分失效——恶意页面无法读取你存在另一站的 Token,也设置不了自定义头(会触发预检且预检失败)。这正是 2.4 节推荐 Token 方案的深层理由。但仍要设防的场景:继续依赖 Cookie 鉴权的接口(老系统、需要浏览器自动携带的场合)。防线三选一或叠加:
SameSite Cookie 属性:声明 Cookie 只在同站请求携带。设为 Lax 或 Strict 后,来自别的站点的跨站请求不再自动带 Cookie——一行属性解决大半问题,现代浏览器已默认 Lax。
CSRF Token:服务端给表单或页面下发一次性随机令牌,提交时校验。恶意页面猜不到令牌,伪造失败。
校验 Origin 与 Referer:服务端检查请求来源是不是自己的站。作为兜底层与其他手段叠加。
三层的关系:SameSite 是默认开机的门锁,CSRF Token 是保险柜,Origin 校验是门口的监控——叠加使用,别单点依赖。
| 威胁 | 利用什么 | 核心防线 | 辅助防线 |
|---|---|---|---|
| 数据窃取(跨域读取) | 同源策略边界 | CORS 白名单由服务端配置 | 不开放通配符加凭证 |
| XSS | 数据被当代码执行 | textContent 默认、富文本白名单净化 | CSP 响应头、HttpOnly Cookie |
| CSRF | 浏览器自动带 Cookie | Token 鉴权或 SameSite 属性 | CSRF Token、Origin 校验 |
| 接口滥用 | 接口裸奔可被脚本直调 | 服务端鉴权与限流 | 频控、验证码、行为风控 |
还有一条贯穿性的纪律值得单列:前端的一切校验都不是安全边界。客户端校验(3.2 节)是给用户的体验提示,攻击者绕过页面直调接口毫无门槛——每个接口必须在服务端重新校验权限与输入。同样,"接口地址保密"也不是防线,网络面板里一切裸奔。
正确姿势是配开发代理——开发服务器把接口请求转发到后端,浏览器看到的是同源请求。错误姿势是关浏览器安全策略或后端开全通配符,前者伤自己机器,后者容易带着上生产。
不能。HTTPS 防的是链路上的窃听与篡改,XSS 与 CSRF 利用的分别是"代码注入"与"身份借用",都发生在加密信道正常工作的情况下。三者的防线互不替代。
没有绝对安全的浏览器存储:localStorage 可被 XSS 读取(所以 XSS 防线必须扎实),内存变量最短命但刷新即失,HttpOnly Cookie 读不到但自动携带又回到了 CSRF 面。常见折中:短时效访问令牌放内存、长效刷新令牌放 HttpOnly Cookie——把两边的弱点错开。
把本章内容转成一份可以在自己项目上直接执行的安检清单,建议按序走,前紧后松。第一步,渲染面扫描:全库搜索所有把数据写进 HTML 的调用点,逐处确认内容来源——可信静态片段放行、纯文本字段改 textContent、富文本路径检查净化库调用与白名单配置。第二步,头与 Cookie 配置核查:响应头里 CSP 是否声明、脚本源是否收紧;会话 Cookie 是否带 HttpOnly 与 SameSite;这两项是"一行配置一层防线"的高性价比检查。第三步,跨域配置审计:拉出所有 CORS 相关配置,确认白名单制、无通配符加凭证组合、预检缓存已配置、暴露的自定义头逐项列明。第四步,鉴权与校验抽查:随机抽五个写操作接口,用工具直调(绕过页面),验证未登录与越权场景是否被服务端拒绝——任何"前端挡住了就行"的接口都是裸的。第五步,依赖与净化库:净化库与安全相关依赖保持更新,历史上这类库的绕过漏洞出现过多次,版本停留是隐性风险。
清单执行的一个常见坑要提醒:自查容易做成"自我表扬"——检查项在脑子里过一遍就打勾。有效的做法是留下证据:每步截图或记录(搜索命中的行号清单、配置的原文、直调接口的响应),汇总成一页报告。有证据的自查才经得起复盘,也方便下次对照"上次的问题修了没"。安全工作没有"完成态",只有"最近一次检查是什么时候"。给安检定个节律——季度做一遍全清单、依赖更新时做对应单项、新功能上线前做渲染面抽查——让它像还房贷一样规律。规律感本身就是防御:攻击者找的是没人看的那扇窗,而你的目光定期扫过每一扇。安检节律之外,给每个高危改动(跨域配置、净化白名单、鉴权中间件)设置双人评审的硬门槛——这类配置一行笔误就是全站洞开,多一双眼睛的成本,远低于一次事故的学费。