前端安全防护实战:XSS、CSRF、点击劫持的系统性防御

前端安全防护实战:XSS、CSRF、点击劫持的系统性防御
神经蛙前端早已不只是”切图”。用户最先接触的页面就是攻击面:一处 innerHTML 用错、一个 Cookie 少写了 HttpOnly,就可能把会话凭据、甚至整个账号交到攻击者手里。本文从同源策略出发,把 XSS、CSRF、点击劫持 三大高频漏洞的成因、利用链与防御一次讲透,并附上可直接落地的 CSP 配置与速查清单。
一、先看懂浏览器同源策略
同源 = 协议 + 域名 + 端口,三者完全一致。这是浏览器安全的基石,它的核心作用是:限制一个源的脚本读取另一个源的资源。
| 操作 | 是否受同源策略限制 | 说明 |
|---|---|---|
| 发送跨域请求(表单 / img / script) | 否 | 可以”发出”,只是默认读不到响应 |
| 读取跨域 AJAX 响应 | 是 | 需目标站 CORS 显式放行 |
| 读取跨域 iframe 的 DOM | 是 | 需同源或 postMessage |
| 设置/读取跨域 Cookie | 受限 | 受 SameSite、Domain、Secure 约束 |
关键认知:同源策略拦截”读取”,不拦截”发送”。正因为如此,<img src="https://bank.com/transfer?to=evil&amount=10000"> 这类请求能”发出去”——这就是 CSRF 能成立的根因。
二、XSS:跨站脚本注入
XSS 的本质是:把攻击者可控的内容,当作代码在当前页面执行。
2.1 三种类型与差异
| 类型 | 触发位置 | 典型载荷入口 | 是否持久化 |
|---|---|---|---|
| 存储型 | 页面渲染时执行 | 评论、昵称、文章正文 | ✅ 持久存在数据库 |
| 反射型 | 请求参数直接回显 | 搜索词、错误提示 | ❌ 随请求消失 |
| DOM 型 | 前端 JS 写入 DOM | location.hash、innerHTML |
❌ 不经过服务端 |
2.2 一条典型的利用链
1 | // ❌ 危险:把 URL 参数直接塞进 innerHTML |
只要用户点开这行链接,Cookie 就会被打包发往攻击者服务器。
2.3 现代防御:不靠”过滤关键字”
① 输出编码要”看上下文”
| 输出位置 | 正确做法 |
|---|---|
| HTML 文本 | 转义 < > & " ' |
| HTML 属性 | 值必须加引号 + 转义 |
| JS 变量 | 用 JSON.stringify 序列化 |
| URL 参数 | encodeURIComponent |
② 富文本用白名单净化,而不是正则
1 | import DOMPurify from 'dompurify'; |
③ 杜绝”字符串拼 HTML”,优先用安全 API
1 | // ✅ 文本一律用 textContent |
④ 开启 Trusted Types(浏览器级兜底)
1 | Content-Security-Policy: require-trusted-types-for 'script'; trusted-types default dompurify |
开启后,任何把未经 Trusted Types 包装的字符串赋给 innerHTML、eval、script.src 的行为都会被浏览器直接拦截,从根上堵死 DOM 型 XSS。
⑤ Cookie 加 HttpOnly,让脚本根本读不到会话 Cookie:
1 | Set-Cookie: SESSION=abc123; HttpOnly; Secure; SameSite=Lax; Path=/ |
三、CSRF:跨站请求伪造
CSRF 与 XSS 相反:攻击者不需要注入脚本,而是借用你已登录的 Cookie,在你的身份下发起”你不知情”的请求。
3.1 攻击长什么样
用户登录银行后未退出,访问了恶意页面,页面里藏着:
1 | <!-- 一个自动提交的隐藏表单,冒充用户转账 --> |
浏览器会自动带上 bank.com 的 Cookie,服务端看不出这是伪造请求。
3.2 四层防御(建议叠加)
① SameSite Cookie —— 成本最低的第一道闸
1 | Set-Cookie: SESSION=abc123; HttpOnly; Secure; SameSite=Lax |
| 取值 | 跨站场景是否带 Cookie | 适用 |
|---|---|---|
Strict |
❌ 一律不带 | 后台管理、支付 |
Lax(默认) |
仅安全的顶级导航(GET)带 | 大多数站点推荐 |
None |
✅ 带(必须配 Secure) |
需要跨站嵌入的登录态 |
② CSRF Token —— 无状态服务端的双保险
1 | // 服务端下发随机 token(存 Session 或签名),前端放入请求头 |
服务端校验:请求头里的 token 必须与会话中的一致。攻击者的站点读不到你的 token(受同源策略限制),因此伪造请求注定失败。
③ 校验 Origin / Referer
1 | # nginx:对状态改变类请求校验来源 |
④ 关键操作二次验证:转账、改密、支付等高风险动作,追加短信/密码/生物识别确认,让”静默伪造”无法完成。
四、点击劫持:看不见的”透明层”
攻击者把你的页面用 iframe 嵌进自己的页面,再叠一层透明按钮,诱导用户点击——用户以为在领红包,实际点的是你的”确认转账”。
| 防御手段 | 指令 | 评价 |
|---|---|---|
X-Frame-Options |
DENY / SAMEORIGIN |
老牌,兼容好,但只能整站粒度 |
CSP frame-ancestors |
'none' / 'self' + 域名 |
现代推荐,可精细到来源 |
| frame-busting JS | if (top !== self) top.location = self.location |
弱,可被双 iframe 绕过 |
1 | Content-Security-Policy: frame-ancestors 'none'; |
两点常被忽略:frame-ancestors 会覆盖 X-Frame-Options,两者同时配置时以 CSP 为准;如果你的站点需要被可信站点嵌入,请用 frame-ancestors https://partner.com 精确放行,而不是直接开放。
五、三类同样危险、却常被忽视的风险
5.1 开放重定向
1 | // ❌ 直接跳转到参数指定地址,可被用于钓鱼 |
5.2 postMessage 未校验来源
1 | // ❌ 任何页面都能给你发消息并操控页面 |
5.3 依赖供应链与原型污染
前端项目动辄上千个 npm 包,一次投毒就能波及所有用户。
1 | # 定期审计与自动修复 |
1 | // ❌ 合并对象时不设防,可被 __proto__ 污染全局原型 |
六、CSP 落地实战:从”只上报”到”强制拦截”
CSP(内容安全策略)是前端的”最后一道总闸”,能在 XSS 已经发生时限制脚本执行与数据外带。
第一步:先用 Report-Only 只上报,观察不拦截
1 | Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report |
第二步:确认无大规模误报后,转为强制策略
1 | add_header Content-Security-Policy " |
上线 CSP 的三个坑:① 尽量别用 unsafe-inline,改用每次请求随机的 nonce;② 灰度期间务必保留 Report-Only 一段时间收集误报;③ 内联事件(onclick="...")会被 CSP 干掉,需提前迁移到 addEventListener。
七、前后端安全速查清单
| 风险 | 前端负责 | 后端 / 运维负责 |
|---|---|---|
| XSS | 上下文输出编码、DOMPurify、Trusted Types | 存储前净化、CSP 响应头 |
| CSRF | 请求头带 Token、校验响应 | SameSite Cookie、Token 校验、Origin 校验 |
| 点击劫持 | — | frame-ancestors / X-Frame-Options |
| 开放重定向 | 跳转白名单 | 服务端二次校验 |
| postMessage | 校验 origin / 指定 targetOrigin |
— |
| 依赖供应链 | npm audit / npm ci |
私有源、签名校验 |
| 原型污染 | 过滤 __proto__ 等危险键 |
入参 schema 校验 |
安全没有”银弹”,只有纵深防御:编码是第一道、CSP 是第二道、Cookie 属性是第三道。任何单点防护都可能被绕过,叠加起来才可靠。
如果把这篇的要点浓缩成一句话:永远不要信任用户输入,也永远不要信任前端过滤——服务端才是最终防线,但前端要做好自己那道门。














