浏览器防护:CSRF、CORS、安全头与路径防火墙
服务端已经知道“这是alice的会话”,仍需判断“这次状态修改真由alice所在页面发起吗”。认证、CSRF、CORS和资源授权分别回答不同问题。本页固定Security7.0.8/Framework7.0.10/Boot4.0.9,配套真实本机实验;身份与会话使用认证会话正文,MVC层CORS处理继续参照MVC横切边界。
CSRF的前提是浏览器自动带上凭据
设受害者已经登录本网站,恶意页面构造一个向本网站的POST表单。浏览器可能自动附带会话cookie,服务端仅凭cookie认证就把操作当作用户意图,这就是需要防范的跨站请求伪造。攻击不以能读响应为前提,所以“浏览器跨域读不到结果”不能反驳资金/邮件地址已被修改。表单内容类型和简单请求也不保证先有预检。
同步令牌方案使页面取得一个攻击源无法凭空获知的随机值,状态修改时随请求参数或指定header提交,服务端与可信仓库中的值校验。默认HttpSessionCsrfTokenRepository使用会话保存token;请求参数通常为_csrf,header通常为X-CSRF-TOKEN,最终以实际repository/handler配置为准。GET/HEAD/TRACE/OPTIONS默认不作为需要令牌的状态修改请求;因此业务不能把扣款/删除放在GET并期待CSRF过滤器兜底。
无session不自动免疫:浏览器自动复用的Basic凭据、自动携带的认证cookie也可能满足攻击前提。真正只由应用代码显式设置Authorization且不自动跨站附带的bearer场景,可按威胁模型分析特定链的CSRF策略;仍需处理XSS和令牌泄漏。本票没有为通过测试关闭CSRF,而是分别验证缺令牌、错令牌、有效令牌和认证后换令牌。
延迟加载、XOR与认证后的令牌变化
Security6及后续默认CSRF token可延迟加载,读取某个请求并不必然立刻创建session;当页面渲染/端点访问CsrfToken时才可能需要底层值。默认XorCsrfTokenRequestAttributeHandler把暴露给页面的值随机掩码,缓解压缩响应侧信道中的重复秘密。每次页面输出字符串不同不等于仓库token每次都变,也不应把掩码值与存储值直接做字符串相等断言。
工程的公开取令牌端点明确访问CsrfToken,让脚本可获得当前parameterName、headerName和token,再用同一个cookie客户端提交。实验返回的是当前handler支持的呈现值,只在内存发送,不把token写到公共证据。浏览器表单链使用默认session仓库;API用CookieCsrfTokenRepository、HttpOnly的XSRF-TOKEN、Path=/api和默认XOR handler,脚本读取JSON呈现值并随同cookie提交X-XSRF-TOKEN,不需读取HttpOnly cookie原值。SPA若选择可由JavaScript读取cookie以及header传递,需要按固定版本配置匹配的request handler,不能把原始cookie值随意当作任何模式的呈现值。
登录成功和注销通过相应策略清除旧CSRF token。测试应在登录成功后再取一次token,然后提交业务POST;特意提交登录前的旧token应被拒绝。自定义成功处理器、SPA页面缓存和跨标签页交互都要处理这个生命周期,不应见到403就把csrf disable。失败定位顺序是cookie是否同一会话→token仓库→handler解析方式→header/参数名→是否经过认证/注销旋转。
HttpOnly限制脚本直接读取cookie,Secure限制在安全通道发送,SameSite约束跨站cookie携带;这三项都不是CSRF令牌的同义词。SameSite的具体行为还取决于浏览器、站点概念、导航/请求方法和兼容策略;不能把“跨源”和“跨站”混为一谈。XSS已能在可信源执行脚本时,通常也可能读页面令牌/发合法请求,需输出编码、内容策略等另外防护。
固定7.0.8 CsrfFilter连续107–136行:先安装DeferredCsrfToken并交requestHandler;不匹配保护条件时继续。需要保护时取得仓库值、由handler解析请求值,再恒定时间比较;失败直接由自己的AccessDeniedHandler处理并返回,未继续业务链。固定来源。Missing/Invalid区分还依赖仓库值是否刚生成,不能只看客户端有没有写一个header。
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
DeferredCsrfToken deferredCsrfToken = this.tokenRepository.loadDeferredToken(request, response);
request.setAttribute(DeferredCsrfToken.class.getName(), deferredCsrfToken);
this.requestHandler.handle(request, response, deferredCsrfToken);
if (!this.requireCsrfProtectionMatcher.matches(request)) {
if (this.logger.isTraceEnabled()) {
this.logger.trace("Did not protect against CSRF since request did not match "
+ this.requireCsrfProtectionMatcher);
}
filterChain.doFilter(request, response);
return;
}
CsrfToken csrfToken = deferredCsrfToken.get();
String actualToken = this.requestHandler.resolveCsrfTokenValue(request, csrfToken);
if (actualToken != null && this.logger.isTraceEnabled()) {
this.logger.trace(LogMessage.format("Found a CSRF token in the request"));
}
if (!equalsConstantTime(csrfToken.getToken(), actualToken)) {
boolean missingToken = deferredCsrfToken.isGenerated();
this.logger
.debug(LogMessage.of(() -> "Invalid CSRF token found for " + UrlUtils.buildFullRequestUrl(request)));
AccessDeniedException exception = (!missingToken) ? new InvalidCsrfTokenException(csrfToken, actualToken)
: new MissingCsrfTokenException(actualToken);
this.accessDeniedHandler.handle(request, response, exception);
return;
}
filterChain.doFilter(request, response);
}BasicAuthenticationFilter在本版本不执行CsrfAuthenticationStrategy,不能把表单认证成功的token清理规律无条件套到Basic每次请求。API的cookie仓库和提交值必须配对;令牌重新呈现的随机性不等于认证导致仓库轮换。
CORS先处理预检,再谈身份与权限
跨源脚本发送某些方法/自定义header前,浏览器先发OPTIONS预检,携Origin、Access-Control-Request-Method和可选header声明;预检通常没有登录cookie。若安全认证先把预检送到登录页,就会出现“账号明明登录,前端始终跨域失败”。通过http.cors(...)使用明确CorsConfigurationSource,让CORS处理处于适当位置。
本实验明确允许一个合成前端Origin、GET/POST和必要header,并允许credentials;同时验证不在允许列表内的Origin。带凭据时allowedOrigins("*")与浏览器约束不相容;应指定可信源,或谨慎使用经核查的origin patterns,不能无条件把任意Origin反射成Access-Control-Allow-Origin。来源列表必须包含scheme/host/port,http://localhost:3000与http://127.0.0.1:3000不是同一个源。
预检允许只告诉浏览器后续跨源请求在该策略下可尝试,不会创建Authentication,不会通过业务授权,也不等于实际请求免CSRF。测试应先验证合法预检无凭据可得到正确允许头,再发实际匿名/缺CSRF请求并观察拒绝;不允许源的预检应失败,恢复为允许源应重新成功。
CORS不是阻止所有客户端访问的服务器防火墙:curl/其他服务并不因没有允许响应头就不能发包。通常也不能防止浏览器发出无需预检的跨站表单,只是限制脚本读取。反过来CSRF token正确也不代表某Origin获准读敏感响应。浏览器执行效果应以真实浏览器上下文验证,HTTP测试只证明服务端收到预检/实际请求后的响应策略。
默认安全头与需要显式选择的策略
Security的HeaderWriterFilter按所选链写响应头;不能只在Controller成功分支手写几个header,而漏掉登录页/拒绝响应。默认常见头包括Cache-Control/Pragma/Expires的禁止缓存组合、X-Content-Type-Options: nosniff、X-Frame-Options: DENY,以及安全请求上的Strict-Transport-Security。实际以固定版本writer、应用已有响应头和链配置为准;应用已显式设置的缓存策略可影响默认writer是否覆盖。
nosniff要求浏览器按声明类型处理,不会自动修正服务器错误的Content-Type;frame限制降低点击劫持面,却可能影响受信嵌入,要选择CSP frame-ancestors等适当策略。缓存控制保护敏感响应,但对公开静态资产可以显式长期缓存并版本化,不能把所有内容永久no-store当性能设计。旧X-XSS-Protection不能作为现代XSS保护的主手段,默认关闭其旧式过滤行为也不等于允许XSS。
HSTS只应通过受信HTTPS响应建立策略;默认writer根据secure请求条件写入。普通loopback HTTP没有HSTS是可解释的条件,不是为了截图给HTTP补一个头就“完成HTTPS”。经过反向代理时secure信息依赖受信转发边界,不能让任意客户端提交的X-Forwarded-Proto直接控制安全判断。本实验普通HTTP即使带伪造转发scheme也没有HSTS;另起真实loopback TLS,客户端只信任本次临时证书并做主机名校验,观察HTTPS响应上的HSTS。证书/keystore随本次流程删除;没有运行生产证书、浏览器HSTS预加载或代理信任链。
CSP没有一个适合所有应用的自动完整默认策略,须按页面资源配置,例如限制default-src、script-src、object-src、base-uri和frame-ancestors,并决定nonce/hash与报告策略。示例应用的显式策略只覆盖它的合成页面;直接给现有站点加script-src 'self'可能破坏内联脚本,Report-Only可以先收集影响但不等同强制阻止。CSP不是输出编码、授权或依赖安全的替代。
StrictHttpFirewall在业务映射之前拒绝歧义
Servlet容器与代理对重复斜线、路径参数、编码斜线、反斜线或点段的解释可能不同。安全matcher认为是公开路径而业务路由认为是管理路径时,就可能产生绕过。FilterChainProxy先经HttpFirewall包装/检查请求,再选择安全链;StrictHttpFirewall倾向于拒绝非规范/歧义输入,而不是随意规范化后继续。
默认方法允许集合有限,TRACE等应按具体版本和需求审查。分号、编码斜线/双斜线、反斜线、编码百分号等各有独立配置开关;为了兼容一个客户端就广泛allow所有形式会扩大解释差异。路径检测要同时考虑原始URI、servletPath/pathInfo、容器是否已经解码以及反向代理是否先重写,不靠Controller里对路径再replace一次解决。
真实测试发送一个容器可以转交、但StrictHttpFirewall明确拒绝的歧义路径,断言400并确认未进入业务;再发送规范路径验证恢复。如果容器在Security之前就拒绝,只能报告容器400,不能冒领为firewall分支证据。原始socket/curl与浏览器会有不同URL规范化行为;需保留实际发出的路径和拒绝来源。调试日志可保留已脱敏的拒绝类型,不把完整查询凭据写入日志。
Host/header名称/值与参数名/值校验也在相应firewall能力范围,版本新增限制以固定源码核对。它不会自动发现SQL注入、业务越权、恶意文件内容或所有请求走私组合。若业务确需合法分号路径,应先建立代理/容器/matcher/Controller一致的解释契约,再精确放开对应项并增加负例,而不是关闭防火墙。
固定7.0.8 StrictHttpFirewall连续515–525行在创建FirewalledRequest前依次拒绝方法、黑名单URL、不可信Host和非规范路径。固定来源。抛出的RequestRejectedException走FilterChainProxy拒绝处理器,不是业务Controller的参数错误。
@Override
public FirewalledRequest getFirewalledRequest(HttpServletRequest request) throws RequestRejectedException {
rejectForbiddenHttpMethod(request);
rejectedBlocklistedUrls(request);
rejectedUntrustedHosts(request);
if (!isNormalized(request)) {
throw new RequestRejectedException("The request was rejected because the URL was not normalized.");
}
rejectNonPrintableAsciiCharactersInFieldName(request.getRequestURI(), "requestURI");
return new StrictFirewalledRequest(request);
}按攻击条件验证,再验证恢复
公开入口使用随机本机端口和独立cookie客户端,依次取得token、错密/正确登录、会话旋转、状态修改、跨源预检、路径拒绝与注销。只记录状态、允许头、认证类型及断言结果;token/cookie不进入公开JSON。失败后恢复使用合法token、允许Origin和规范URL,确认业务仍可用,防止“所有请求都拒绝”被误判为安全功能通过。
| 改变的输入 | 需要观测的行为及排障点 |
|---|---|
| 有会话无token或错误token POST | 拒绝;确认发生在CsrfFilter而非业务验证失败 |
| 同会话当前token POST | 正常;再用认证前token对照旋转,不用禁用防护取得200 |
| 无凭据的允许Origin OPTIONS | CORS允许头正确且没有凭空建立身份 |
| 不允许Origin OPTIONS、随后合法Origin | 前者拒绝、后者恢复;不能只看最终200 |
| 合法CORS但实际请求缺身份/令牌 | 仍由认证/CSRF/授权拒绝,说明几个判断独立 |
| HTTP与真实TLS请求 | HTTP及伪造转发头无HSTS,信任本次证书且校验主机名的HTTPS有HSTS;不把本机证书外推生产信任链 |
| 歧义路径与规范路径 | 明确拒绝层与未执行副作用,规范路径恢复 |
这些是本机行为验证,不证明所有浏览器版本、反向代理、SSO和生产部署无漏洞。负载/会话/撤销方案的量化取舍见认证会话页的固定模型:缩短有效期可能增加登录/哈希成本,缓存身份可能增加撤销窗口;安全默认值也要和资源恢复责任一起验证。
官方来源与版本边界
本页的契约来自固定7.0.8 Servlet exploits目录、CORS集成及Security7.0参考,查询2026-10-26。浮动小版本目录用于发现,精确分支以正文固定tag源码及实验依赖树为准。旧6.3.3的过滤链/异常说明保留其原版本,7.0配置使用lambda DSL,迁移不能靠复制旧.and()链式示例。
浏览器学习站点的代码复制、搜索、自测/回查、刷新保存由桌面Chrome验证;实验HTTP客户端对CORS的结果只代表服务端协议响应,不冒充真正恶意浏览器场景和漏洞审计。真实TLS已在loopback临时证书条件下运行;生产代理信任链、多节点会话及持久remember-me数据库不在本票实跑记录中。
闭卷验收
本页 10 道闭卷题收进了下方自测面板:先回忆,再展开参考答案与评分点,最后给三档自评。