
上个月接手了一套混布了不少年的老系统安全扫描报告里挂了一串很眼熟的条目Session Cookie Missing Secure Attribute、Cookie Without HttpOnly Flag、SameSite Cookie Not Set点名的组件统一是Tomcat而且是7到9好几个版本混在一起。这类问题在扫描器里通常只算中危优先级排得并不高但真要顺着攻击链往下想每一处都够让人冒冷汗会话标识能被脚本读走、能被跨站请求顺手带走、还能跟着明文HTTP裸奔。这篇把我在多版本Tomcat环境里排查和修复会话Cookie安全属性缺失的完整过程写出来包括不同版本之间的行为差异、每个版本该改哪里、老版本不支持SameSite时怎么兜底以及改完之后怎么验证才能不误伤业务。正在处理安全扫描报告或者打算自查会话Cookie加固策略的同学可以直接把它当操作手册用。1. 漏洞全景缺的是三个属性漏的是三类攻击1.1 三个属性各自管住哪一段路先说Secure。这个属性告诉浏览器这条Cookie只能在HTTPS连接里带着走。一个站点如果同时开放了HTTP和HTTPS入口或者攻击者和用户处在同一个局域网里就有机会在用户发起请求时把流量降级到HTTP或者诱导浏览器去加载一个HTTP资源。这时候没有标Secure的会话Cookie会老老实实跟着明文请求发出去网络里任何能抓包的人都能直接读到JSESSIONID。很多人觉得“我只要全站都跳HTTPS就够了”但实际上很多系统的TLS是在Nginx这一层终结的到后端Tomcat时走的还是HTTP外部访问的强制跳转也并不能覆盖所有入口和所有历史客户端。Secure属性从根上约束了浏览器让会话Cookie绝不降级到明文链路这是它无法被HSTS替代的地方。然后是HttpOnly。这个属性跟XSS是配套关系它禁止脚本通过document.cookie读取Cookie但并不会阻止XSS注入本身。也就是说你的站点里只要有一个能执行脚本的注入点攻击者下一步自然就会去试着读Cookie。没有HttpOnly一次本来只影响前端的XSS漏洞可以直接升级成会话劫持加了HttpOnly脚本拿到的是一串空值攻击者要继续偷会话就得换DOM劫持、钓鱼、或者更复杂的链路攻击成本立刻上来。Tomcat对容器自己创建的Session Cookie默认会输出HttpOnly但在老版本、被改过配置、或者应用自己创建Cookie的场景下并不保证自动带上。SameSite是三者里最容易被忽视的。它控制浏览器在跨站请求时要不要携带这条Cookie。Lax模式下只有用户从外部站点点链接进来这类顶层导航GET请求会带Cookie跨站表单POST、图片请求、iframe子页面都不会带Strict连顶层导航都不带None放开限制跨站请求愿意带就带但浏览器通常要求必须同时有Secure属性。没有SameSite时攻击者可以在自己的页面上放一个自动提交的表单指向你系统里的修改密码接口浏览器会把未设SameSite的会话Cookie一起带过去服务端看到的就是一个合法的登录用户在自己发请求。CSRF的经典打法很大程度就是靠这一下。1.2 为什么扫描结果经常是三个属性一起缺我处理过很多次类似的扫描报告发现这几个条目经常是成批出现背后其实有一套固定的版本和配置原因。Tomcat对Session Cookie的默认策略大概是这样HttpOnly默认开Secure默认不开SameSite在老版本里完全没有实现在新版本里默认值也是不输出。所以一个没有做过任何加固的Tomcat实例抓下来的Set-Cookie响应头通常只有Path/最多再带一个HttpOnlySecure和SameSite基本是铁的缺失。如果以前为了兼容某个老系统有人显式把useHttpOnly关掉那三个属性一起缺也很正常。更隐蔽的一个场景是会话不由Tomcat自己管。很多项目用了Spring Session、Shiro这类框架Session Cookie由框架生成或者直接在业务代码里通过response.addCookie()写自定义Cookie。这种时候Tomcat的CookieProcessor、web.xml里的session-config都对它们不生效。很多团队在Tomcat配置里折腾半天回头一抓包发现响应头纹丝不动原因多半就在这里你修的这条Cookie根本不是容器创建的。1.3 风险定级这类中危为什么值得认真对待扫描器把Cookie安全属性缺失标记成中危是有道理的单个属性缺失并不等于系统已经被打穿。但它是一个非常典型的放大器型问题。一个后台管理系统如果同时还存在一处存储型XSS管理员会话Cookie又没有HttpOnly攻击者只需要让管理员打开一个带payload的链接Session ID就能被脚本读走如果Cookie没有Secure攻击者又能和用户在同一个内网再配一次协议降级会话就能在明文链路里被嗅探。系统越重要、登录态价值越高这个组合拳的杀伤力就越接近高危。尤其在外网可达、多租户、承载交易这类场景下别因为扫描器给的是中危就一直挂账。把Secure、HttpOnly、SameSite三个属性补齐投入其实非常小换来的是整个认证链路最基础的一道护栏。2. 多版本差异先定位你的Tomcat到底支持到哪一步2.1 版本能力对照速查动手之前先花两分钟确认Tomcat版本和运行模式这一步能省掉后面大量的无效排查。不同版本的默认行为和可配置项差距非常大我按实际见到的主流版本整理了一张速查表最终判断以你手里那台实例的响应头和catalina.log为准。Tomcat版本默认HttpOnly默认Secure是否支持配置SameSite7.0.x是否不支持需过滤器兜底8.0.x是否不支持需过滤器兜底8.5.x早期是否不支持需过滤器兜底8.5.x后期是否视具体小版本而定以实测为准9.0.x9.0.40之前是否不支持需升级或过滤器兜底9.0.40及以上是否支持通过CookieProcessor配置10.x / 11.x是否支持通过CookieProcessor配置这里的“默认HttpOnly是”指的是容器生成的Session Cookie大多默认带HttpOnly但如果你在web.xml的cookie-config里显式关过或者管理员改过useHttpOnly结果就不一样了。Secure在所有版本里默认都是关的。SameSite是最容易踩坑的点不要说Tomcat 7了就是Tomcat 8.5早期你在context.xml里写一个samesite属性容器压根不认识严重的直接启动报错。所以先认清版本能力再选方案别看到配置示例就抄。2.2 属性到底由哪一层决定一条Session Cookie最终长成什么样不是单点配置能决定的。Tomcat内部大致是这样一条链路Connector负责协议层Context维护useHttpOnly这类通用开关真正拼装Set-Cookie响应头的是CookieProcessor。容器创建JSESSIONID的时候会把Context的配置交给CookieProcessor由它决定最终输出哪些属性。应用自己创建的Cookie则是通过HttpServletResponse.addCookie()把Cookie对象丢给同一个CookieProcessor而Cookie对象自带的属性优先级要高于容器级默认值。所以排查顺序一定要反过来。先拿一个真实的登录响应看Set-Cookie到底长什么样判断这条Cookie是谁创建的再决定改容器配置还是改应用代码最后根据版本选可行路径。我见过太多人上来就照着某篇博客往conf/context.xml里塞配置塞完发现不生效就开始怀疑Tomcat坏了。其实多半要么是版本支持不了要么是配置写在了优先级更低的地方要么根本就是业务框架自己发的Cookie。2.3 用一条命令快速判断当前状态找一个能返回Set-Cookie的登录或登录页接口直接把响应头拉出来看一眼。前后端分离项目就找登录成功返回Header的接口传统页面应用直接访问登录页就行只要后端创建了Session响应头里一定会有Set-Cookie。curl -sk -i https://your-host/your-app/login \ -H Content-Type: application/json \ -d {username:readonly,password:temp} | grep -i set-cookie正常场景下拿到的JSESSIONID后面跟着什么属性一眼就能判断缺口。然后再对照一下conf/context.xml和应用自己的META-INF/context.xml确认全局配置和应用级配置谁覆盖了谁。如果你用的是内嵌Tomcat比如Spring Boot项目那两个XML文件都不存在得去application.yml里翻server.servlet.session.cookie.*这一段。另外顺手分享一个排查技巧整套系统如果有很多台Tomcat别一台一台去敲命令。写个几行的Shell循环把所有节点的Set-Cookie响应头抓下来横向对比哪台机器配置漂移基本当场现形。我在实际排查中经常发现同一个应用在不同版本的Tomcat下Cookie属性居然不一样这就是版本差异在背后慢慢积累出来的坑。3. 修复实操按版本选路径按环境定参数3.1 全局配置优先Tomcat 9.0.40及以上的标准做法如果确认实例是Tomcat 9.0.40以上或者10.x/11.x最省事的方案是直接改conf/context.xml让这台实例上的所有应用都吃到同一套配置。在Context节点里加一个CookieProcessor像这样Context useHttpOnlytrue CookieProcessor classNameorg.apache.tomcat.util.http.Rfc6265CookieProcessor sameSiteCookieslax / /ContextsameSiteCookies取值可以是unset、none、lax、strict默认是unset表示不输出SameSite属性。选lax作为默认最稳妥它在安全和体验之间最均衡用户从外部链接点进来这种顶层导航GET请求不受影响跨站表单、fetch这类自动请求不会带Cookie。如果业务明确没有跨站跳转场景可以上strict。none要非常谨慎它允许跨站携带Cookie等于自己把CSRF的防御面拉大了而且浏览器要求None必须配合Secure也就是说你这条链路必须能确保全程HTTPS。如果你用的是Spring Boot内嵌Tomcat没有context.xml这个文件配置落在application.yml里。新版本的Boot已经映射了对应的Session Cookie属性大致是这样server: servlet: session: cookie: http-only: true secure: true same-site: lax这里有一个实操中很容易掉进去的优先级问题应用自带的META-INF/context.xml优先级高于全局的conf/context.xml。你明明改了全局配置却没效果先去看看对应应用目录下有没有这个文件里面是不是把useHttpOnly关掉了或者又写了一个别的CookieProcessor。这种“改了不生效”的案例十有八九出在这个优先级上。3.2 老版本处理能升级就升级升不了就上过滤器兜底Tomcat 7、8.0、8.5早期没有内置的SameSite配置能力这里要分两种情况处理。一种是你允许做小版本升级比如Tomcat 7升到比较新的7.0.x补丁版8.5升到后期支持SameSite的小版本升级完以后再尝试配置Rfc6265CookieProcessor和sameSiteCookies。这种升级范围小、兼容风险可控值得优先做。另一种是升级牵扯到太多应用依赖短期内动不了版本那就必须在应用层兜底。应用层通用方案是写一个Filter用HttpServletResponseWrapper包住响应拦截所有Set-Cookie响应头逐个追加缺失的Secure、HttpOnly、SameSite。这个方案基于Servlet标准接口不依赖Tomcat内部API所以在老版本上一样能跑。我用的示例代码大致是这样package com.example.security; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponseWrapper; import java.io.IOException; import java.util.regex.Pattern; public class CookieSecurityFilter implements Filter { private static final Pattern HAS_ATTR Pattern.compile( (?i)(^|;\\s*)(secure|httponly|samesite)(\\s*|\\s*;|$)); Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { if (response instanceof HttpServletResponse) { HttpServletResponse httpResponse (HttpServletResponse) response; chain.doFilter(request, new CookieSecurityResponseWrapper(httpResponse)); } else { chain.doFilter(request, response); } } private String upgrade(String headerValue) { if (headerValue null || headerValue.isEmpty()) { return headerValue; } if (HAS_ATTR.matcher(headerValue).find()) { return headerValue; } StringBuilder sb new StringBuilder(headerValue); sb.append(; Secure); sb.append(; HttpOnly); sb.append(; SameSiteLax); return sb.toString(); } static class CookieSecurityResponseWrapper extends HttpServletResponseWrapper { CookieSecurityResponseWrapper(HttpServletResponse response) { super(response); } Override public void addHeader(String name, String value) { if (set-cookie.equalsIgnoreCase(name)) { value upgrade(value); } super.addHeader(name, value); } Override public void setHeader(String name, String value) { if (set-cookie.equalsIgnoreCase(name)) { value upgrade(value); } super.setHeader(name, value); } } }upgrade里的正则判断是为了避免对已经带属性的Cookie重复追加。比如某个Cookie本身已经有SameSiteNone你无脑再拼一个SameSiteLax上去重复的属性会导致浏览器解析行为不可预期有的浏览器直接拒绝这条Cookie。实际生产里如果只想对JSESSIONID做处理可以把匹配逻辑收到更细只针对特定Cookie名操作如果像我这样统一处理那就对所有Set-Cookie一视同仁。Filter在web.xml里注册到/*让它在所有响应链路的最前端包住业务代码filter filter-namecookieSecurityFilter/filter-name filter-classcom.example.security.CookieSecurityFilter/filter-class /filter filter-mapping filter-namecookieSecurityFilter/filter-name url-pattern/*/url-pattern /filter-mapping需要注意这个Filter是在给所有Cookie加Secure如果你的实例既有HTTPS访问又有HTTP访问HTTP环境下浏览器会直接丢弃Secure Cookie导致Session来回漂移。所以上线前务必把访问链路理清楚是不是所有入口都确保HTTPS是不是代理已经终结了TLS但Tomcat不知道外部是HTTPS。3.3 别忽略web.xml本身的能力不要因为能配置CookieProcessor就放弃web.xml两者配合使用效果更好。对容器自己创建的Session CookieServlet规范从3.0开始就提供了标准配置项只要容器实现了规范都认。在应用的web.xml里写上session-config session-timeout30/session-timeout cookie-config http-onlytrue/http-only securetrue/secure /cookie-config tracking-modeCOOKIE/tracking-mode /session-config这段配置只对Session Cookie有效业务代码里response.addCookie()创建的自定义Cookie不受控。需要再次强调securetrue/secure的副作用如果应用在本地或内网用HTTP调试Secure Cookie不会被浏览器保存每次请求都会服务端重新创建Session表现就是页面一直登录不上排查起来很容易误判成“改坏了”。我自己的习惯是正式环境部署包开Secure测试开发环境用独立的环境变量或构建参数关掉不要把开发测试和正式放到同一份配置模板里写死。3.4 面对多种Cookie诉求别企图用一把钥匙开所有锁有的系统里会有好几类Cookie一部分要求SameSiteStrict另一部分必须是None全局一个CookieProcessor根本接不住这种粒度。这种场景下我的建议是先梳理这些Cookie到底都是谁创建的哪些是框架自动生成的哪些是业务代码写的。适合的方式是让框架层各扛各的Spring Session有专门的cookie-same-site配置Shiro在Session Cookie属性上也有接口优先用框架自带的开关去调比在Tomcat层做一个无差别全量修改干净得多。Tomcat配置用来兜住容器默认Session Cookie这条基线框架配置负责业务自定义Cookie两边各归各后面维护才不会互相打架。4. 改完以后验证、回归、排雷4.1 打一个裸请求别被浏览器缓存骗了修复后第一件事重新抓登录接口的Set-Cookie。我习惯用curl因为可以完全控制不带任何旧Cookiecurl -sk -i https://your-host/your-app/login \ -H Content-Type: application/json \ -d {username:readonly,password:temp} | grep -i set-cookie正常结果应该是类似Set-Cookie: JSESSIONID6D9D8C...; Path/; HttpOnly; Secure; SameSiteLax顺序不重要关键看三个属性都在。浏览器端也可以顺手验证打开开发者工具的Network面板选中登录请求翻Response Headers里的Set-Cookie。这里最容易犯的错误是用一个已经登录过的浏览器去访问页面页面靠着旧Cookie跑得一切正常响应头即使已经变了你也完全察觉不到。所以每次改完配置都用一个无痕窗口或者curl做一次不含任何旧Cookie的裸请求。4.2 改了没生效按这个优先级排查碰到Set-Cookie仍缺属性别先怀疑自己写错按下面顺序走一遍。第一应用自身有没有META-INF/context.xml覆盖了全局配置或者web.xml里把http-only显式关成了false。第二访问链路确实是HTTPS吗Secure属性有没有因为代理层是HTTP而根本没被添加。第三Session Cookie真是Tomcat创建的吗项目里如果有自定义会话框架容器配置管不到它。第四确认版本支持你写的配置属性老版本遇到未知属性可能直接启动失败去catalina.log里找找有没有class not found或解析异常。第五如果你是Spring Boot检查外部化配置到底加载了哪个application.yml别改了一个不生效的空文件。我用的一张速查表分享出来现象最可能原因优先排查点配置了但响应头完全没变应用级配置覆盖全局或Session由框架接管META-INF/context.xml框架配置只有Secure不一致代理层HTTP回源Tomcat不知道外部HTTPSConnector的scheme/secure/proxyPortSameSite没生效版本太老属性不被识别升级版本或上Filter改完以后登录失效Secure导致HTTP下Cookie被浏览器丢弃开发环境关闭Secure正式确认全链路HTTPS只有部分接口生效Filter映射顺序或后缀没覆盖检查web.xml的url-pattern和filter顺序4.3 开启Secure后登录失效多半是代理链路问题这个坑在真实环境里反复出现。Nginx做了HTTPS终结转发给后端Tomcat时用的是HTTPTomcat的request.isSecure()直接返回false于是它生成的Session Cookie就不会带Secure。如果你在context.xml里强行开了securetrue浏览器又只在HTTPS页面上保存Cookie于是整个Session在来回跳转中就丢了。解决方向是让Tomcat感知到外部连接其实是HTTPS。可以在Connector上加schemehttps securetrue proxyPort443或者用RemoteIpValve配合Nginx传来的X-Forwarded-Proto头。改动之后看一眼Tomcat访问日志里客户端isSecure的状态配合浏览器状态确认到底是哪层在起作用。如果只是正式环境需要Secure开发测试环境不需要把这个差异放到部署环节去处理比在Tomcat配置里写死要干净得多。4.4 SameSite调整后的业务回归点SameSite从无到有对绝大多数页面应用无感但两种场景容易踩坑。一种是跨站单点登录或第三方回调另一种是内嵌iframe的页面。Lax模式下第三方站点通过POST表单回跳到你的系统浏览器不会带Session Cookie用户会发现自己明明已经登录第三方了跳回来却不认登录态。Strict更明显用户从外部App里点一条链接进站都可能丢失会话。如果业务确实需要这类跨站会话保持优先把回调路径改成顶层导航GET实在改不了再在风险评估后对回调域名相关Cookie单独设置SameSiteNone; Secure并且服务端一定要配合CSRF Token和来源校验。不要为了省事把所有Cookie都设成None那等于把CSRF的门重新打开了。4.5 把验证做成自动化别等下一次扫描报告手动抓一次响应头解决当下扫描但下次上线呢会不会又缺很可能会。所以我的建议是写进发布CI的一个小脚本对每个环境跑一遍同样的curl检查所有Set-Cookie里是否包含Secure、HttpOnly、SameSite缺一个就fail掉这次发布。脚本很简单本质上就是把上面的grep换成循环判断。另一个思路是在运维侧统一管配置用配置管理工具下发统一的context.xml模板应用侧在项目骨架里默认带上Cookie安全Filter。无论走哪条路目标是让“Cookie三个属性齐全”成为系统默认值而不是入职一个新同事、开一台新服务器、搭一个新环境都要手工补一遍。安全修复最怕的就是修完当天没事三个月后有人用老模板开了新环境问题原样复活。5. 从修复到加固一些更长期的经验5.1 别只修Tomcat这一层整条链路都得看Tomcat把属性补齐只是后端这道门前面还有反向代理、负载均衡、网关这一层层设施。如果代理层会重写Set-Cookie头或者存在某条旧链路还在用HTTP回源那Tomcat加了Secure也保不齐在入口处被改掉。我在修完Tomcat之后一定会拿公网入口的域名再抓一次全链路响应头确认和直连后端拿到的属性一致。同时顺手检查一下站点有没有启用HSTS避免用户第一次访问还停在HTTP时就先暴露一次Cookie。Cookie安全属性不是单点问题从入口到出口全链路保持一致才算真正修完。5.2 升级Tomcat大版本时把历史包袱一起清掉Tomcat 7到Tomcat 9或10除了Cookie安全属性还会遇到Servlet API包名从javax.servlet变成jakarta.servlet这种大变动、TLS配置方式变了、默认Session Cookie路径规则也不同。如果本来就在计划从7或8.5升级不要只盯着Cookie属性修复先把部署包、依赖库、框架版本全部列出来统一做一轮回归再切生产。升级过程中顺手把useHttpOnly这种显式开关查一遍很多旧配置是从远古时代抄下来的翻出来一看写着useHttpOnlyfalse也不奇怪。这种东西升级之前不清升级之后又会成为下一份扫描报告里的老面孔。5.3 安全修复的日常心态处理这类漏洞和修功能Bug最大的区别在于修完你什么都感觉不到。页面还是正常加载数据库还是正常读写没有报错也没有变化很容易让人认为“白改了”。但一旦哪天真的遇上一次XSS或者CSRF被利用这几个属性就是能不能守住最后一道线的关键。所以我一直把它们当成基础设施像灭火器、消防通道一样平时用不上关键时候能救命。安全上的小修小补恰恰是最不能因为“看不见效果”就拖着不管的部分。最后分享一个我自己的习惯不管最后选了哪种修复方案都留一个能一键回到修复前状态的对比方式。比如在CI里跑一个对比脚本把修复前后的Set-Cookie输出贴到同一份记录里下次给审计看或者做漏洞复测时能直接把“属性已补齐”这句话变成肉眼可见的证据。这个改动本身不大但它是判断一个系统整体健壮性最直观的体检项之一。