网站缓存漏洞的典型表现,是用户A的页面被用户B看到,或者登录后的私有数据被错误地缓存在公共节点上。这类问题通常不来自缓存技术本身,而来自缓存策略配置不当、缓存键设计粗糙、响应头缺失或CDN规则过于宽泛。修复的核心不是禁用缓存,而是让缓存精确地知道什么能存、存多久、谁能取。
先定位缓存边界,再谈修复
修复前需要明确一个基本事实:并非所有内容都适合缓存。静态资源如图片、CSS、JavaScript文件可以长时间缓存,但包含用户会话、订单信息、个人资料的HTML响应必须禁止公共缓存。很多漏洞的根源,就是把这两类内容混在同一套缓存规则里。比如某些反向代理默认缓存所有200响应,只要URL相同就返回同一份内容,完全忽略Cookie和Authorization头。攻击者只要构造一个不带登录态的请求,就可能拿到缓存中带敏感数据的页面副本。
检查缓存漏洞时,优先确认三个位置:源站响应头、反向代理或CDN的缓存策略、应用代码中手动设置的缓存控制。任何一个位置出现过于宽松的配置,都可能导致数据泄露。例如源站返回了
Cache-Control: public, max-age=3600,但该页面实际包含当前用户的手机号,那么接下来一小时内任何请求这个URL的人都会看到同一份手机号。
修复响应头:从源站切断错误缓存
最直接的修复手段是在源站强制输出正确的缓存控制头。对于包含用户私有数据的页面,应返回:
Cache-Control: private, no-store, max-age=0 Pragma: no-cache Expires: 0
其中
no-store比
no-cache更严格,它要求任何中间节点不存储响应副本。很多开发者误以为
no-cache是不缓存,实际上它表示可以缓存,但每次使用前必须重新验证。对于登录后的页面、购物车、个人中心等,必须使用
no-store。对于可以缓存但仅限单个用户的内容,使用
private并配合短过期时间。
静态资源则应明确标记为公共缓存并设置版本化URL:
Cache-Control: public, max-age=31536000, immutable
版本化URL是关键。文件名中带哈希值,例如
app.a1b2c3.js,这样内容更新时URL自然变化,不会出现旧缓存被继续使用的问题。如果静态资源URL不带版本号,就绝不能设置过长的max-age,否则更新后用户长时间拿不到新文件。
缓存键设计:把用户维度纳入隔离
很多缓存漏洞的根因是缓存键只包含URL,忽略了用户身份。例如CDN默认以
scheme + host + path + query作为缓存键,当两个不同用户访问同一个
/account/settings路径时,CDN会返回同一份缓存。修复方法是把用户身份标识加入缓存键,或者更简单地,对需要区分用户的路径直接禁用公共缓存。
如果业务上确实需要缓存个性化内容,可以在反向代理层配置缓存键包含特定Cookie或Header的哈希值。例如Nginx中可以这样配置:
proxy_cache_key "$scheme$host$request_uri$cookie_user_id";
但要注意,把Cookie直接放入缓存键可能引入新的风险。如果Cookie值可被用户控制,攻击者可以通过构造不同Cookie值来探测缓存中是否存在其他用户的数据。更安全的做法是使用服务端生成的、不可预测的会话标识,并确保该标识本身不包含敏感信息。
另一个常见错误是把Authorization头完全排除在缓存判断之外。API响应如果携带
Authorization: Bearer token,源站却返回
public缓存头,CDN就会把第一个请求的响应缓存下来,后续所有请求无论token是否有效都拿到同一份数据。修复方式是:凡是带Authorization头的请求,响应一律不得公共缓存,除非业务明确允许。
CDN与反向代理层的规则收紧
源站响应头是基础,但CDN和反向代理层的规则同样重要。很多CDN允许配置“忽略源站Cache-Control”或“强制缓存所有内容”,这类选项一旦开启,源站再怎么设置也无济于事。检查CDN配置时,要确认以下几点:
第一,是否开启了“遵循源站缓存头”。如果关闭,CDN会用自己的默认规则覆盖源站指令。第二,是否对带Set-Cookie的响应做了特殊处理。正常情况下,带Set-Cookie的响应不应被公共缓存,因为Set-Cookie意味着响应与特定用户会话相关。第三,是否对带Authorization的请求绕过缓存。第四,是否配置了缓存键排除某些查询参数或Cookie,这些排除规则是否可能把不同用户的内容混在一起。
在反向代理层,比如Nginx,可以通过
proxy_no_cache和
proxy_cache_bypass指令对特定条件强制不缓存:
proxy_no_cache $http_authorization $cookie_session; proxy_cache_bypass $http_authorization $cookie_session;
这样任何带Authorization头或session Cookie的请求都不会命中缓存,也不会把响应写入缓存。对于更复杂的场景,可以使用
map指令定义多个条件变量,集中管理哪些请求允许缓存。
应用代码中的缓存逻辑清理
有些缓存漏洞来自应用层的手动缓存实现,比如用Redis或Memcached存储页面片段或完整响应。这类实现如果缓存键设计不当,同样会造成跨用户数据泄露。常见错误包括:只用URL作为缓存键、把用户ID拼在缓存键中但未做类型转换、或者缓存了包含用户输入的内容而没有做输出编码。
修复应用层缓存时,要确保缓存键包含完整的用户隔离维度。例如用户ID、租户ID、语言偏好、设备类型等任何会影响响应内容的因素都应纳入键中。同时,缓存写入前要检查响应中是否包含
Set-Cookie头,如果有,应避免缓存整个响应,或者只缓存不含会话信息的部分。
另一个容易被忽视的点是缓存数据的过期与失效。当用户修改个人资料或退出登录后,旧的缓存条目如果未及时清除,可能在新请求中继续返回。解决方式是在用户数据变更时主动删除相关缓存,或者使用较短的过期时间配合版本号机制。对于敏感操作,比如密码修改、手机号变更,应在操作完成后立即清除该用户的所有个性化缓存。
验证修复效果的具体方法
修复完成后,不能只靠肉眼确认。需要模拟不同用户身份请求同一URL,观察响应内容是否隔离。具体操作:用两个不同账号分别登录,访问同一个私有页面,对比返回内容是否各自独立。然后退出登录,用匿名身份再次请求,确认不会拿到任何登录态下的内容。同时检查响应头是否按预期返回
no-store或
private。
对于CDN场景,可以通过添加调试头或查看CDN日志确认命中情况。如果匿名请求命中了缓存并返回了登录态内容,说明缓存键或缓存规则仍有问题。还可以使用curl命令手动测试:
curl -I -H "Authorization: Bearer test" https://example.com/api/user/profile
观察返回的
Cache-Control和
Vary头是否正确。如果响应中缺少
Vary: Authorization或
Vary: Cookie,中间缓存可能无法正确区分不同用户。
长期防护机制:把缓存安全纳入开发流程
单次修复只能解决当前问题,长期防护需要把缓存安全纳入代码评审和部署流程。建议在CI/CD中加入缓存头检查,对包含敏感路径的响应自动断言
Cache-Control值。例如测试脚本可以扫描所有返回
Set-Cookie的响应,确认它们同时包含
no-store或
private。对于API接口,检查带Authorization的响应是否禁止公共缓存。
同时建立缓存策略文档,明确哪些路径允许公共缓存、哪些必须私有、哪些完全禁止缓存。文档中要给出具体的响应头示例和CDN配置模板,避免开发者凭感觉设置。对于新上线的功能,评审时把缓存策略作为必查项,特别是涉及用户数据的页面和接口。
监控方面,可以在日志中记录缓存命中率与用户维度的关联。如果某个私有路径的公共缓存命中率异常升高,可能意味着规则被误改或绕过。定期审计CDN配置和源站响应头,防止配置漂移。安全团队应把缓存漏洞纳入常规渗透测试范围,重点测试跨用户数据读取、缓存投毒、缓存欺骗等攻击面。
缓存本身不是漏洞来源,错误的缓存边界才是。修复的关键在于让每一层都清楚知道数据的隐私级别,并严格执行对应的缓存策略。源站响应头、CDN规则、应用代码三者必须一致,任何一层的疏忽都会让整个防护失效。把缓存安全作为系统设计的一部分,而不是事后补救,才能从根本上减少这类问题的复发。
