news 2026/9/11 5:19:32

cURL自定义Host头引发的跨源Cookie泄漏与注入攻击详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
cURL自定义Host头引发的跨源Cookie泄漏与注入攻击详解

1. 项目概述与场景还原

1.1 这个实验是怎么来的

做Web开发的人应该都用过cURL,命令行下调试接口、模拟请求、验证重定向,顺手就敲一行curl -I https://example.com。但有一类参数平时用得不多,真正用的时候就容易翻车,比如-H "Host: xxx"

这个参数允许你手动覆盖HTTP请求里的Host头。正常浏览器发请求,Host头是自动填的,你无法修改。但cURL作为一个开发者工具,把这个开关留给了用户。问题是,一旦你可以自定义Host头,它带来的连锁反应远不止"改了个请求头"那么简单。

我这个项目就是从一次线上事故开始的。某个内部系统在测试环境复现用户反馈时,前端页面一直登录态异常,后来排查发现是测试脚本里写死了自定义Host头,把带Cookie的请求打到了错误的虚拟主机上,结果Set-Cookie写错了作用域,整个会话被污染。后来我把这个场景单独拎出来复现、分析,越查越觉得这里面的坑值得专门写一篇。

如果你平时会在命令行里用cURL调试多域名或虚拟主机环境,或者你在写自动化脚本时习惯加Host头来访问测试环境,这篇文章值得看完。它能帮你搞明白:自定义Host头到底会影响什么,Cookie是怎么在这种场景下被"跨源"读写的,以及攻击者如何利用这个特性做注入。

1.2 自定义Host头的正常用途

先说说大家平时为什么加 Host 头。

最常见的场景是本地联调。后端服务部署在开发机上,Nginx配置了多个虚拟主机,你本地的hosts文件没有同步开发机的域名映射,于是就直接用IP访问,再通过-H "Host: dev.example.com"告诉Nginx你要访问的是哪个站点。这个做法非常普遍,不少老工程师的习惯就是curl -H "Host: dev.example.com" http://192.168.1.10/

还有一类场景是测试WAF规则、验证CDN回源策略、或者模拟特定域名的访问行为。比如你想确认生产域名在某种特定请求头组合下是否会被拦截,直接改Host头模拟一遍是最快的。

但这里有个容易被忽略的细节:cURL自定义Host头只改了请求行里的Host字段,并没有改Cookie、Referer、Origin等任何其他字段。这就是问题开始的地方。

一个Web应用判断"我是谁",通常依赖几个信息:Host头、请求协议、访问路径。而Cookie的归属,则由Domain和Path属性决定。当Host头和Cookie携带的域信息不一致时,服务端和浏览器都会陷入一种"身份混乱"状态。你以为是访问example.com,实际上请求被路由到了另一个虚拟主机上,但Cookie还是example.com的——数据就是这么流出去的。

2. 跨源Cookie泄漏的底层逻辑

2.1 Cookie的Domain属性决定"是谁的Cookie"

要理解Cookie为什么会泄漏,先要弄清楚Cookie的本质。Cookie是服务器扔给浏览器的一段键值对数据,但它不是无主的数据。服务器在返回Set-Cookie响应头时会指定Domain和Path,浏览器收到后会做一个判断:这个Cookie应该挂在哪个域下面。

比如服务器返回:

Set-Cookie: session_id=abc123; Domain=.example.com; Path=/

那么浏览器就会把session_id这个键值对挂在.example.com下面,访问任何以example.com结尾的域都会带上这个Cookie。但如果Domain是shop.example.com,那么它只会在访问shop域的时候被携带。

这里有个关键点:Cookie的判断依据是浏览器的"当前访问地址",而不是响应里Host头或者业务逻辑。浏览器不会关心你HTTP请求里Host头到底填了什么,它只关心地址栏里的域名是什么。

那问题就来了:如果你的服务端代码里,生成Cookie时用了Host头作为Domain属性的值,而Host头是用户可控的,会发生什么?

我写了个最小复现,服务端逻辑假设是这样的Java伪代码:

String host = request.getHeader("Host"); Cookie cookie = new Cookie("token", randomToken()); cookie.setDomain(host); // 直接用Host头当Domain cookie.setPath("/"); response.addCookie(cookie);

你正常访问https://a.com,Host头是a.com,生成的Cookie Domain也是a.com,一切正常。但攻击者可以构造一个请求:

curl -H "Host: attacker.com" https://a.com/login

如果后端没有对Host头做白名单校验,那么服务端就会返回一个Domain为attacker.com的Cookie。虽然这个Cookie是a.com的应用生成的,但浏览器会把它当作attacker.com的Cookie存起来。这就完成了第一次"注入"——你在a.com的应用里,给attacker.com种了一个Cookie。

2.2 Host头改写如何绕过同源策略

紧接着第二层问题:跨源读取。

同源策略是浏览器的安全基石,要求协议、域名、端口完全一致才允许相互访问Cookie和DOM。但Host头改写这种发生在HTTP协议层面的操作,完全不经过浏览器的同源策略判断。因为cURL不是浏览器,它不会强制执行Same-Origin Policy,它只会忠实地把请求发出去。

举个例子,你本机跑着两个服务:

  • 服务A:监听80端口,虚拟主机绑定api.internal.com
  • 服务B:监听80端口,虚拟主机绑定public.web.com

你直接用公网IP访问,默认情况下Nginx匹配不到任何虚拟主机,会走默认server块。但如果你加上-H "Host: api.internal.com",请求就会被路由到服务A。此时如果你在这个请求里携带了原属于public.web.com的Cookie,服务A照样能收到。Cookie本身不会自检"我是不是应该发给这个Host",它只会按照浏览器或客户端的规则决定要不要附带。

用cURL模拟这个过程非常简单:

curl -v -H "Host: api.internal.com" \ -H "Cookie: session_id=secret-token-from-public-web" \ http://127.0.0.1/

服务A的后端日志会完整记录到session_id=secret-token-from-public-web。这就是典型的跨源Cookie泄漏——Cookie原本属于公网Web域,却因为一个自定义Host头被送到了另一个内部服务的怀里。

2.3 代理与转发过程中的Cookie路径

真实生产环境里,客户端很少直连业务服务器,中间一般还有Nginx、HAProxy、云负载均衡等代理层。代理层在处理Host头时,行为差异很大,这会让Cookie泄漏问题更隐蔽。

常见的代理配置有这么几种:

  • 透传Host头:代理层不修改Host头,原样转发给后端。如果代理层根据其他字段(比如IP+端口)做路由,后端拿到的是客户端定义的Host头。
  • 重写Host头:代理层强制把Host头改成后端真实域名,比如proxy_set_header Host $host;改成proxy_set_header Host api.internal.com;
  • 自定义header透传:很多微服务网关规定了一个约定,客户端不能直接改Host,但可以通过X-Forwarded-Host等自定义头传递原始Host。

透传模式的问题最严重。假设负载均衡器根据"URL前缀+真实IP"路由请求,后端有两套应用分别绑定不同域名。攻击者只要找到负载均衡能访问到的一个内网地址,配合自定义Host头,就可以把带Cookie的请求导向任意后端。整个过程中,代理层不会去校验Host头是否与后端域名匹配,它只做了转发。

更隐蔽的是路径型泄漏:当反向代理配置了基于路径的转发,比如/api/转发到服务A,/转发到服务B,而服务A用Host头决定生成Cookie的Domain时,攻击者构造一个Host: evil.com的请求访问/api/anything,服务A生成的Cookie Set-Cookie就带有evil.com域。如果这个响应通过了CDN缓存,还会影响其他用户。这个链路我后面用具体实验演示。

3. Cookie注入的攻击面与复现过程

3.1 从Host头到Set-Cookie的注入点

服务端Set-Cookie的Domain字段,是一个容易被忽略的反射型注入点。现在很多框架已经默认用配置项而不是Host头来设置Domain,但历史代码里用request.getServerName()$_SERVER['HTTP_HOST']拼Cookie Domain的比比皆是。

用Go语言标准库写个例子,很多人初学时会这样写:

func loginHandler(w http.ResponseWriter, r *http.Request) { token := generateToken() http.SetCookie(w, &http.Cookie{ Name: "auth", Value: token, Domain: r.Host, // 问题行 Path: "/", }) w.Write([]byte("ok")) }

r.Host直接取了请求中的Host字段。此时我用一条cURL命令就能给任意域种Cookie:

curl -i -H "Host: evil.example.net" http://target.com/login

返回的响应头里会出现:

HTTP/1.1 200 OK Set-Cookie: auth=token_value; Domain=evil.example.net; Path=/; HttpOnly

虽然这个Cookie是target.com的应用生成的,但它的Domain被设置成了evil.example.net。浏览器只要收到过这次响应,就会把这个Cookie保存到evil.example.net名下。后续攻击者在自己的evil.example.net站点上,可以通过JavaScript读取这个Cookie(如果没标记HttpOnly更是如此),或者配合子域攻击使用。

这就是"注入"的本质:不是往SQL里注入,而是往Cookie的归属空间里注入了一枚不属于你的凭证。

3.2 会话固定攻击路径

Cookie注入最常见的危害是会话固定(Session Fixation)。

正常流程下,用户在a.com完成登录,服务端创建一个会话,返回的Session ID被写入浏览器。但攻击者可以提前通过自定义Host头,让a.com生成一个攻击者已知的Session ID,并让它落在victim.com域下。受害者访问victim.com时,因为攻击者种下的Cookie Domain匹配,浏览器就会自动带上这个已知的Session ID。

完整的攻击路径我整理成了一张表:

步骤操作者动作结果
1攻击者curl -H "Host: victim.com" http://a.com/logina.com返回Domain=victim.com的Cookie,攻击者知道其值
2攻击者诱导受害者访问a.com的一个URL受害者浏览器收到Set-Cookie,将Cookie保存在victim.com域下
3受害者登录victim.com,服务端复用已有Session ID攻击者用已知Session ID直接进入受害者会话
4攻击者使用之前的Session ID请求victim.com接口完成会话劫持

这个攻击链最关键的点是:攻击者在没有任何跨域脚本能力的情况下,通过一个HTTP请求就完成了对另一个域名的Cookie预置。而且由于Cookie是一个服务端响应Set-Cookie设置的,浏览器完全信任它,不会发出任何跨域告警。

3.3 实际复现:一个最小模拟环境

为了验证这个攻击链,我搭了一个非常简单的模拟环境。两台服务挂在同一个Nginx下,分别绑定a.testv.test,核心逻辑是后端用Host头设置Cookie。

Nginx虚拟主机配置:

server { listen 80; server_name a.test; location / { proxy_pass http://127.0.0.1:8081; } } server { listen 80; server_name v.test; location / { proxy_pass http://127.0.0.1:8082; } }

后端A代码如下(Node.js):

const http = require('http'); http.createServer((req, res) => { const host = req.headers.host; res.setHeader('Set-Cookie', `session=abc123; Domain=${host}; Path=/`); res.end('login ok'); }).listen(8081);

然后我执行攻击请求:

curl -i http://127.0.0.1/login -H "Host: v.test"

注意这里指IP访问,Nginx仍然根据Host头路由到了a.test的server块,但后端代码读取到的Host是v.test,于是返回:

Set-Cookie: session=abc123; Domain=v.test; Path=/

这个Cookie落在v.test域下。我再正常访问v.test的登录接口,就能看到浏览器把之前种下的Cookie带过去了。整个过程没有跨域脚本,没有浏览器漏洞,纯粹是服务端信任了用户可控的Host头。

v.test上的后端B如果是这套逻辑:

const http = require('http'); http.createServer((req, res) => { console.log('cookie:', req.headers.cookie); res.end('v site'); }).listen(8082);

那么攻击者构造一条带上session=abc123的请求,就能直接被v.test后端当成合法会话处理。

4. 实操记录与问题排查技巧实录

4.1 复现环境的完整搭建步骤

上面那个实验,完整过程我记录一下,方便你自己在本地复刻。

环境准备就一个Linux或macOS,不需要联网,装好Docker或者本地有Node/Python都可以。以下用Docker方式演示,依赖最少。

第一步,写一个Dockerfile,装Node环境和Nginx。说实话直接拉镜像更省事:

docker run -d --name curl-host-lab -p 8080:80 nginx:alpine echo "模拟完成"

第二步,进入容器,改Nginx配置,添加两个server块。容器里没有vim,用sed或者直接挂载配置文件都行。我习惯本地写配置再挂载,方便反复修改。目录结构是这样的:

lab/ ├── nginx.conf └── app/ ├── a.js └── v.js

第三步,分别启动两个Node后端,再启动Nginx,完整的启动脚本我放在一个run.sh里:

#!/bin/bash node app/a.js & NODE_PID_A=$! node app/v.js & NODE_PID_B=$! nginx -c $(pwd)/nginx.conf echo "A和B服务已启动"

整个过程踩坑点有几个:本地hosts不用改,因为全走IP;容器内Nginx如果是在本机跑,listen 80不会和Node冲突(Node监听不同端口);重要的是一定要让Nginx按Host头分流,确认后端收到的是自定义Host而不是Nginx自己生成的Host。

4.2 验证Cookie注入的完整命令清单

环境起来之后,我用的验证命令和输出记录在此。

场景一:证明Set-Cookie Domain可控

curl -i http://127.0.0.1/login -H "Host: evil.test"

输出节选:

HTTP/1.1 200 OK Server: nginx/1.21.6 Content-Type: text/plain Set-Cookie: session=abc123; Domain=evil.test; Path=/

这里有三个关键点要注意:

  • 如果后端在Nginx后面,proxy_pass http://127.0.0.1:8081;不加任何改写,Host头不会被Nginx篡改,此时后端能完整读到evil.test
  • 如果Nginx写了proxy_set_header Host $host;,那么$host是Nginx请求行里的值,也就是IP,后端收到的Host就变成IP了,注入会失败。这一点可以用来做防御验证。
  • 如果Nginx写了proxy_set_header Host $http_host;,则会透传完整Host头,包括端口,注入依然成功。

场景二:验证Cookie被跨源携带

curl -i http://127.0.0.1/v -H "Host: v.test" -H "Cookie: session=abc123"

后端V的日志里会显示出收到了来自evil.test种的Cookie。此时注意:访问v.test时你主动携带了Cookie,但在真实浏览器场景里,应该模拟"浏览器自动携带"。这个更准确的方法是用一个脚本先拿到Set-Cookie,再把它存到Cookie Jar里,然后访问v.test看是否自动带上。cURL的Cookie Jar用法:

curl -c cookies.txt -i http://127.0.0.1/login -H "Host: evil.test" curl -b cookies.txt -i http://127.0.0.1/v -H "Host: v.test"

如果cookies.txt里记录的Domain是evil.test,而第二条命令访问的Host是v.test,cURL会做域名匹配,理论上不会自动带。但我们在后端代码写的是Node原生解析,它不做域名匹配,请求头里有什么Cookie它就打印什么。这个细节容易让实验结果失真,所以我在脚本里手动把Cookie加进了第二条请求的Header里——这种方式模拟的是攻击者在后续请求中直接注入已知Cookie。

场景三:用真实浏览器验证

命令行工具验证到这一步,只能证明HTTP层面数据流,要证明浏览器的行为,还得借助真实浏览器。我做了个简单的HTML,在a.test下发起fetch请求到v.test接口(这里注意CORS配置),观察Network面板里的Cookie变化。实测下来比较麻烦,因为浏览器的Secure、SameSite策略会干预。实验环境如果是http而不是https,SameSite=Lax默认会在跨站场景下拦掉Cookie。所以在模拟真实攻击链时,最好在同一主域的子域之间做实验,比如evil.testv.test无法共享Cookie的话,换成a.example.comv.example.com即可。

4.3 实战中常见的报错与坑位

排查这个实验的过程中,我遇到了几个非常典型的报错,写下来给后来人避坑。

cURL 35错误

curl: (35) schannel: next initialize security context failed: SEC_E_INVALID_TOKEN,这是在Windows上用cURL访问https站点时最常见的错误,原因是本地系统的证书信任库或者schannel与后端TLS配置不兼容。遇到这个错误,先别怀疑代码,先确认后端是不是TLS握手有问题。临时绕开可以用-k参数跳过证书校验,但只能用于本地实验。

cURL 56错误

curl: (56) recv failure: connection reset by peer,这个一般是服务端主动断连,最常见的原因就是后端的虚拟主机配置不匹配,Nginx找不到对应的server块直接返回400或者关闭连接。特别是添加了自定义Host头之后,如果Nginx里没有配置对应的server_name,默认会走第一个server块,有时行为会让人迷惑。

有一个最小的诊断技巧:

curl -v -H "Host: no-such-host.test" http://127.0.0.1/

如果返回400或者connection reset,多半是Nginx的默认server没配置好。加一个默认server块返回明确错误码,后续排查会快很多。

Cookie带不上

模拟浏览器自动携带Cookie的场景时,最容易踩坑的是cURL的-b cookies.txt-H "Cookie: xxx"混用。cURL会优先用-H指定的Cookie,如果同时指定了cookie jar,行为不一定符合预期。我建议一次只用一个方式,别混用,否则排查时不知道Cookie到底来自哪里。

Node后端读不到Host

Docker环境里,Node监听的是127.0.0.1,而Nginx也在容器里,proxy_pass是127.0.0.1没问题。如果你把Node跑在宿主机,Nginx跑在容器里,就要用宿主机IP去配proxy_pass,否则就超时。这个和Host头本身无关,但实验时很容易卡在这里。

SameSite干扰

最后说一个最容易被忽略的点:现代浏览器默认的Cookie SameSite策略是Lax,跨站请求默认不携带Cookie。这个会直接影响你对"跨源"的验证效果。如果你要复现的是"跨站"场景,需要把Set-Cookie里加SameSite=None; Secure,但Secure又要求https。实验环境如果不是https,这个配置会让Cookie直接被浏览器丢弃。最稳妥的做法是:

  • 做"跨子域"实验,比如Cookie Domain设置为.example.com,访问sub1.example.comsub2.example.com
  • 做"跨源"实验,用两个完全不同的顶级域名,这时Cookie不会被浏览器自动带上,但攻击者可以通过脚本或代理场景强制携带。

5. 防御建议与安全编码实践

5.1 服务端如何正确校验Host头

这个问题的根源是"信任了不该信任的输入"。服务端对Host头的信任,就像在登录表单里信任了用户输入的手机号——你以为那是用户自己的号码,其实是可以随便填的。安全的做法包括:

第一,设置Host白名单。现代主流框架都有allowedHosts之类的配置。以Spring Boot为例,配置项:

server: forward-headers-strategy: framework

然后用ServerProperties里的host校验。Node的Express社区有host-validation中间件,Nginx层面可以通过默认server返回403来兜底。

第二,不要直接使用request.getServerName()r.Host这类原始Host值作为业务逻辑输入。要么从代理层注入的X-Forwarded-Host取(前提是代理层已经清理过),要么从配置中心读取自己的域名。生成Cookie时,Domain固定写死,不要动态拼。

第三,代理层要显式重写Host头。Nginx配置里:

server { listen 80 default_server; server_name _; return 403; } server { listen 80; server_name api.example.com; proxy_set_header Host api.example.com; proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Proto $scheme; proxy_pass http://backend; }

第一段default_server直接拦截所有非我域名的请求,从入口处杜绝了自定义Host头的注入。第二段将Host头强制写死,后端无论怎么读,都不会读到客户端传来的值。

5.2 开发与测试中的cURL使用安全清单

作为开发者,日常用cURL调试时,也要养成好习惯。我在踩过坑之后给自己总结了一份清单,分享出来:

  • 本地调试虚拟主机时,优先改本地hosts文件,而不是用-H "Host"。比如echo "127.0.0.1 dev.example.com" >> /etc/hosts,然后直接curl http://dev.example.com/。这样Cookie域、Host头、TLS证书全部一致,最接近线上真实状态。
  • 如果必须用-H "Host",记得同时加上--resolve参数。比如curl --resolve dev.example.com:80:127.0.0.1 http://dev.example.com/,这种方式既能保证Host正确,又能保证请求确实发到目标IP,比手动指定IP加Host头更安全。
  • 涉及Cookie的测试,用cookie jar别手动拼Cookie。curl -c cookies.txt -b cookies.txt http://example.com/login,这样能看到浏览器视角下的Cookie存取行为,而不是凭直觉乱带。
  • 凡是发出去的请求带了Cookie,务必确认Host头是真实业务域名。我在自动化脚本里写过一个检查函数,发现Host头包含ip地址或者非白名单域名就直接抛错,避免无意识的密钥泄露。

5.3 额外补充:关于Cookie的安全属性

最后补充一下Cookie本身的安全属性。即便你修复了Host头注入问题,Cookie的安全配置依然要到位。只用Domain和Path控制可见范围是不够的,还需要关注:

属性作用建议
Secure只在HTTPS请求中携带生产环境一律开启
HttpOnly禁止JavaScript读取需要防XSS的Cookie必须开启
SameSite控制跨站携带策略默认Lax;业务上无跨站需求就直接Strict
DomainCookie作用域尽量精确到当前域名,不要随便用上级域
PathCookie作用路径能窄就别宽,/是最后的选择

这几项配合Host白名单,才能形成完整的纵深防御。单一修复某一层,在实际攻击链里可能还是会留下缺口。

从实验的角度看,这个项目最有价值的收获不是"发现了某个工具的bug",而是让我们重新审视了HTTP协议中一个"看起来无辜"的字段到底承载了多少信任。cURL的自定义Host头功能本身没有错,错的是服务端把Host当成了可信标识。换个角度看,任何能控制HTTP请求的应用层工具,都具备类似的能力,不只是cURL,也包括Postman、Burp Suite等。理解了这个机制,你在写服务端代码的时候就会多想一层:这个Header是谁在控制?它能被我信任吗?

我自己的习惯是,写完一段读取Host、处理Cookie的代码之后,先用一条cURL命令做负向测试——故意传一个不存在的Host,看服务端是否会生成一个带有攻击者可控域的Cookie。这个习惯帮我拦下了好几次潜在隐患,你可以试试。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 5:19:29

Qwen3源码证据驱动评测:大模型开源仓库的静态工程审阅

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 5:19:21

西门子PLC矿井通风控制系统:IO表、引脚图与程序设计详解

矿井通风系统对PLC程序的要求,往小了说就是“怎么把风机转起来、停下来”,往大了说涉及安全联锁、故障保护、配电逻辑、变频调速、上位机通信。标题里关键词很明确:西门子PLC、矿井通风控制系统、IO表、PLC引脚图、PLC程序设计,还…

作者头像 李华
网站建设 2026/9/11 5:18:13

无线传感器网络安全多跳传输Matlab仿真方案

1. 项目概述:无线传感器网络中的安全传输挑战 在野外环境监测、工业设备状态监控等实际场景中,无线传感器网络(WSNs)常常面临两个关键挑战:一是信号在长距离传输过程中的硬件噪声干扰,二是敏感数据可能被恶…

作者头像 李华
网站建设 2026/9/11 5:17:29

RabbitMQ高可用镜像队列实战:从集群搭建到生产故障恢复指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 5:16:59

LLM判官稳定性排查指南:输入、提示、模型、解析四维调优

1. 项目概述:当“判官”开始打摆子,我们到底在评什么?“判官不稳定时,先别换判官”——这句话刚在内部评测群里刷出来,我就盯着屏幕愣了三秒。不是因为措辞犀利,而是它精准戳中了当前LLM-as-Judge实践里最常…

作者头像 李华