跨域与安全
约 1104 字大约 4 分钟
跨域CORS安全
2024-08-13
同源策略
概念
同源策略 (Same-Origin Policy) 是浏览器的安全机制,限制来自不同源的文档或脚本与当前源的资源交互。
源的定义
协议://域名:端口| URL | 源 |
|---|---|
| https://example.com | 基准 |
| https://api.example.com | 不同域名 |
| http://example.com | 不同协议 |
| https://example.com:8080 | 不同端口 |
受限操作
- Cookie、LocalStorage、IndexDB 访问
- DOM 操作
- AJAX/Fetch 请求
CORS 跨域资源共享
简单请求
满足以下条件的请求为简单请求:
- GET、POST、HEAD 方法
- 仅使用允许的头字段
- Content-Type 为:
application/x-www-form-urlencodedmultipart/form-datatext/plain
GET /api/data HTTP/1.1
Origin: https://example.com
Host: api.other.com
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://example.com
Content-Type: application/json
{"data": "hello"}预检请求 (Preflight)
非简单请求会先发送 OPTIONS 预检:
OPTIONS /api/data HTTP/1.1
Origin: https://example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Content-Type, Authorization
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400服务端配置
Express (Node.js)
const express = require('express');
const app = express();
// 方式一:所有来源
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
next();
});
// 方式二:指定来源
app.use((req, res, next) => {
const allowedOrigins = ['https://example.com', 'https://www.example.com'];
const origin = req.headers.origin;
if (allowedOrigins.includes(origin)) {
res.header('Access-Control-Allow-Origin', origin);
}
next();
});
// 处理预检请求
app.options('*', (req, res) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.sendStatus(204);
});
// 路由
app.get('/api/data', (req, res) => {
res.json({ data: 'hello' });
});Koa
const Koa = require('koa');
const cors = require('@koa/cors');
const app = new Koa();
app.use(cors({
origin: ctx => ctx.header.origin,
credentials: true,
}));
app.use(async ctx => {
ctx.body = { data: 'hello' };
});Nginx
location /api/ {
# 允许的来源
add_header 'Access-Control-Allow-Origin' '$http_origin' always;
# 允许的方法
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
# 允许的头
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization' always;
# 是否允许携带凭证
add_header 'Access-Control-Allow-Credentials' 'true' always;
# 预检缓存时间
add_header 'Access-Control-Max-Age' 86400 always;
# 处理 OPTIONS 请求
if ($request_method = 'OPTIONS') {
return 204;
}
proxy_pass http://backend;
}JSONP
<!-- 客户端 -->
<script>
function callback(data) {
console.log(data);
}
</script>
<script src="https://api.example.com/data?callback=callback"></script>// 服务端
app.get('/data', (req, res) => {
const { callback } = req.query;
const data = { message: 'hello' };
res.send(`${callback}(${JSON.stringify(data)})`);
});代理
开发环境代理 (Vite)
// vite.config.js
export default {
server: {
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
},
};开发环境代理 (Webpack)
// webpack.config.js
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
pathRewrite: { '^/api': '' },
},
},
},
};Nginx 反向代理
server {
listen 80;
server_name example.com;
location /api/ {
proxy_pass http://backend-server:3000/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}Web 安全
XSS 跨站脚本攻击
存储型 XSS
攻击脚本永久存储在目标服务器。
<!-- 用户评论 -->
<script>
// 恶意脚本被存储
</script>反射型 XSS
恶意脚本通过 URL 参数反射到页面。
// URL: https://example.com/search?q=<script>alert(1)</script>
// 页面直接显示搜索结果DOM 型 XSS
纯前端代码通过 URL 操纵 DOM。
// 直接读取 URL 并写入页面
document.write(location.search);防御措施
// 1. 输入过滤
function filterXSS(str) {
return str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
// 2. CSP
// Content-Security-Policy: script-src 'self'
// 3. HttpOnly Cookie
// Set-Cookie: session=xxx; HttpOnly
// 4. 使用 textContent 而非 innerHTML
element.textContent = userInput; // 安全
element.innerHTML = userInput; // 危险CSRF 跨站请求伪造
攻击者诱导用户访问恶意页面,利用用户的登录状态发起请求。
<!-- 恶意页面 -->
<img src="https://bank.com/transfer?to=attacker&amount=10000">防御措施
// 1. CSRF Token
// 服务端生成随机 token,客户端请求时携带
const token = document.querySelector('meta[name="csrf-token"]').content;
// 2. SameSite Cookie
// Set-Cookie: session=xxx; SameSite=Strict
// 3. 验证 Referer
app.use((req, res, next) => {
const referer = req.headers.referer;
if (referer && referer.includes('example.com')) {
next();
} else {
res.status(403).send('Forbidden');
}
});
// 4. 双重提交
// 客户端同时在 URL 参数和 Cookie 中发送 tokenSQL 注入
// 危险
const query = `SELECT * FROM users WHERE id = ${userId}`;
// 安全 - 使用参数化查询
const query = 'SELECT * FROM users WHERE id = ?';
db.execute(query, [userId]);点击劫持 (Clickjacking)
X-Frame-Options: DENY
Content-Security-Policy: frame-ancestors 'none'CSP 内容安全策略
<!-- 多种配置方式 -->
<meta http-equiv="Content-Security-Policy" content="
default-src 'self';
script-src 'self' 'unsafe-inline' https://trusted-cdn.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
connect-src 'self' https://api.example.com;
frame-ancestors 'self';
">| 指令 | 说明 |
|---|---|
| default-src | 默认源 |
| script-src | JavaScript 源 |
| style-src | CSS 源 |
| img-src | 图片源 |
| connect-src | AJAX/WebSocket 源 |
| frame-ancestors | 嵌入页面源 |
常用安全头
// 常见安全响应头
res.header({
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'DENY',
'X-XSS-Protection': '1; mode=block',
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
'Content-Security-Policy': "default-src 'self'",
'Referrer-Policy': 'strict-origin-when-cross-origin',
});