「JavaWeb审计盲点」Spring HTTP 请求方法走私(Method Smuggling)
渗透测试
本文主要分析 了Spring 中 HTTP method 被 HiddenHttpMethodFilter 改写与 HEAD 隐式映射到 GET 这两类机制,结合特定的场景展示了其中可能存在的安全风险。
0x00 前言 ======= 0.1 一个普通的管理系统 ------------- 一个常见的后台订单管理系统,接口设计基于RESTful 风格: - `POST /api/orders/{id}` :创建/更新订单,所有调用方可用 - `DELETE /api/orders/{id}` : 删除订单,危险操作,仅允许拥有对应角色的用户使用 - ...... - `GET /api/ops/ping?host=...` :运维小工具,通过ping命令检测到目标主机的网络连通性,只允许管理员(运维)使用 **具体的接口定义**(`OrderController` / `OpsController`): ```Java @PostMapping("/api/orders/{id}") // 创建/更新订单 @DeleteMapping("/api/orders/{id}") // 删除订单 @GetMapping("/api/orders") // 订单列表(公开) // 运维小工具:测网络连通性,会真实执行系统 ping 命令 @GetMapping("/api/ops/ping") public Map<String, Object> ping(@RequestParam String host) throws Exception { log.info("[Controller] 运维接口: DispatcherServlet 看到的 method=%s, 开始执行 ping %s%n", request.getMethod(), host); Process process = new ProcessBuilder("ping", "-c", "1", "-t", "2", host).start(); ... } ``` 对于RESTful 风格的应用来说,RESTful 语义本来就绑在 method 上。同一路径下 GET 是查询、POST 是创建、DELETE 是删除,权限需求天然不同,"登录用户可建单、只有管理员能删"这种最常见的需求,按路由维度一刀切太过于粗暴,所以SpringSecurity提供了基于Method维度的配置,参考https://docs.spring.io/spring-security/reference/servlet/authorization/authorize-http-requests.html :  通过SpringSecurity完成方法+路由层面的权限配置,创建订单的权限直接放开,删单和运维工具仅管理员可访问: ```Java .requestMatchers(HttpMethod.DELETE, "/api/orders/**").hasRole("ADMIN") .requestMatchers("/api/orders/**").hasRole("USER") .requestMatchers(HttpMethod.GET, "/api/ops/**").hasRole("ADMIN") .anyRequest().permitAll() ``` 0.2 **这套代码真的安全吗?** ------------------ 基于上面的案例代码,简单验证下权限相关的安全性: 正常情况下,未授权访问直接返回401 status:  以普通用户身份尝试登陆后,可以正常查看订单信息:  但是如果想尝试调用DELETE接口,会直接被Spring Security拦截,返回403 status:  包括运维接口同样的也是符合前面设计的权限矩阵,无法越权操作:  从相关HTTP请求的stauts可以看到,确实没办法未授权&越权访问相关的业务接口,但是相关的接口真的安全了么? 0x01 Method Smuggling ===================== 1.1 HEAD走私 ---------- 基于上述的例子,可能有很多师傅第一时间想到如果**通过@RequestMapping定义路由且缺少method 属性时,可以匹配任意 HTTP 方法,**具体代码逻辑在 org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition#getMatchingCondition: 首先会通过CorsUtils.isPreFlightRequest 方法用来判断当前请求是不是浏览器的 CORS 预检请求,若不是,当缺少method属性时,如果请求是 OPTIONS 且不是 ERROR 转发,则返回 null,表示不匹配。 其余任何 method 会调用返回 this,即匹配一切,这也就解释了为什么裸@RequestMapping可以匹配任意HTTP方法了:  但可惜的是,上述接口都明确使用了对应请求方法的注解,例如运维接口`@GetMapping("/api/ops/ping")` 但值得关注的是,如果定义了相关的method属性/使用对应方法的注解,则会调用matchRequestMethod方法进行处理,这里当然也包括类似@DeleteMapping的注解,本质上也是通过@RequestMapping定义相关的属性来处理的:  查看matchRequestMethod方法,这里会发现一个有意思的逻辑: 参考RFC定义https://www.rfc-editor.org/info/rfc9110/#section-9.3.2,HEAD 应当返回与 GET 相同的元数据,意味着服务器必须完整执行 GET 的处理逻辑才能算出正确的 Content-Length、ETag 等响应头。所以 Spring MVC 让 HEAD 方法命中 @GetMapping 并执行处理逻辑、最后由容器统一处理丢掉返回的response body。 也就是说在SpringMVC中,**GET请求的接口同样可以用HEAD方法来访问**:  如果在基于请求方法+路由的权限配置中没有考虑相应的情况,大概率会存在防护绕过的风险,虽然说HEAD请求没办法返回body,对于一些读接口没办法达到利用的效果,但是类似上面案例中的运维接口,直接导致了未授权访问的风险。 在刚刚的运维接口记录对应的请求日志,并尝试通过HEAD方法请求绕过安全防护:  可以看到绕过了SpringSecurity的限制,在非登陆情况下成功访问了ping运维接口,同时成功进入对应的Controller逻辑并执行:  1.2 \_method走私 -------------- 下面看另外一个例子,普通用户登陆后可以看到具体的订单信息:  尝试调用DELETE方法进行删除,因为不是管理员,会返回403 status。 此时尝试将DELETE请求转换成POST,然后请求参数`_method=DELETE`,可以发现,在普通用户的身份下,成功调用了管理员才能操作的删除逻辑,删除了id=1的内容:  重新访问`/api/orders`接口,发现对应的内容确实被删除了:  从接口的定义上看,传递的参数仅仅只有一个id,并没有`_method`:  那么具体是什么原因绕过了SpringSecurity的权限控制呢? 查阅代码,发现在Filter里注册了一个HiddenHttpMethodFilter,这类`框架内置的组件配置`在审计时往往会一带而过,它不是业务代码,看起来只是开了个官方功能,很少有人会追问它在过滤器链上的位置和具体行为。 ```TypeScript @Bean public FilterRegistrationBean<HiddenHttpMethodFilter> hiddenHttpMethodFilter() { FilterRegistrationBean<HiddenHttpMethodFilter> bean = new FilterRegistrationBean<>(new HiddenHttpMethodFilter()); return bean; } ``` 而它恰恰是导致绕过的罪魁祸首,下面看看其具体的实现以及绕过原理。 ### 1.2.1 HiddenHttpMethodFilter HiddenHttpMethodFilter确实是个正常的官方功能。 在HTML表单提交时,一般只支持 GET 和 POST 两种 method。而 RESTful 设计里删除要用 DELETE、更新要用 PUT/PATCH,浏览器表单无法正常发送对应的请求。 HiddenHttpMethodFilter是SpringMVC中的一个过滤器,它允许使用HTML表单来模拟PUT、DELETE和其他HTTP请求方法。它通过解析请求参数中名为`_method`的特殊参数,完成`POST + _method` → `真实 method`的转换。 例如下面的例子:  那么在请求时只需要在POST的基础上,通过访问/api/orders/1?\_method=DELETE即可调用对应的接口逻辑了。 注册的方法除了通过@Bean注入外,还支持以下几种: - 通过配置文件直接开启 ```Properties spring.mvc.hiddenmethod.filter.enabled=true ``` - 在web.xml里注册Filter: ```XML <filter> <filter-name>hiddenHttpMethodFilter</filter-name> <filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class> </filter> <filter-mapping> <filter-name>hiddenHttpMethodFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> ``` ### 1.2.2 绕过原因 查看下org.springframework.web.filter.HiddenHttpMethodFilter的具体实现,主要关注doFilterInternal的实现:  代码逻辑也比较简单,主要是对POST请求进行处理,如果当前请求是POST且请求携带\_method参数,会对\_method参数的值进行匹配,如果在白名单范围内(PUT、DELETE、PATCH),则进行请求转换:  那么为什么这个Filter会影响SpringSecurity的配置呢? SpringSecurity本身也是通过filterChain进行实现的(springSecurityFilterChain),而其中授权动作发生在链内的 `AuthorizationFilter`。这里涉及到一个解析顺序的问题。 在Spring中,对于Filter的执行一般会遵循**小值优先**的原则,从org.springframework.boot.web.servlet.ServletContextInitializerBeans的构造方法可以看到,它负责把所有 filter 注册进容器,然后通过`AnnotationAwareOrderComparator.INSTANCE` 做排序,按照排好的顺序依次注册,order 小的先注册、先进链、先执行:  最后默认的Tomcat容器 按 addFilter 的调用顺序组装过滤器链。 这里通过断点查看整理后的顺序,可以看到springSecurityFilterChain确实在HiddenHttpMethodFilter前面:  这也就解释了为什么通过`POST+_method`的方式可以绕过对应的权限控制了,大致流程如下:  可能会有疑问是,那有没有可能这个顺序是随机的呢?其实这里filter的顺序跟Order属性有关。 案例中HiddenHttpMethodFilter是通过@Bean注入的,而默认情况下的order值为Integer.MAX\_VALUE:  也就是说默认会放到最后面,见https://github.com/spring-projects/spring-framework/blob/main/spring-core/src/main/java/org/springframework/core/Ordered.java:  而springSecurityFilterChain主要是基于 `SecurityProperties.DEFAULT_FILTER_ORDER`的配置,也就是-100,这也就解释了前面断点中,两个filter对应order的值了,也再次映证了绕过的原因:  ### 1.2.3 其他 前面还提到一种方式是,可以通过`spring.mvc.hiddenmethod.filter.enabled=true`配置开启,但是这种方式下的filter顺序会有额外的差异,可以看到HiddenHttpMethodFilter的顺序已经在springSecurityFilterChain之前了:  此时通过`POST+_method`的方式也没办法绕过对应的安全限制:  原因也很简单,通过WebMvcAutoConfiguration#hiddenHttpMethodFilter 可以看到,当spring.mvc.hiddenmethod.filter.enabled=true 生效时,自动配置返回的不是普通的 HiddenHttpMethodFilter,而是子类:  而它的默认值刚好就是-10000:  另外`ALLOWED_METHODS` 白名单(PUT/PATCH/DELETE)是 Spring 5.3 才加的,更早版本几乎不限制,类似 `POST→GET` 也是支持的。 除了上面提到的\_method外,部分框架可能还支持**X-HTTP-Method-Override**头部进行操作,在审计时可以额外关注下。 0x02 总结 ======= HTTP method 在请求的过程中不一定是一个常量,而是一个会被各层框架组件改写,且各层理解不一的变量。 上面提到的SpringMVC中的三种请求Method“走私“通道,本质上还是利用了各层的解析差异: | | | |---|---| | “走私“通道 | 具体原理 | | \_method 走私 | HiddenHttpMethodFilter 在过滤器链上把 POST 改写成 DELETE/PUT/PATCH | | HEAD 走私 | Spring MVC 源码里的隐式规则:GET 映射白送 HEAD([RFC 9110 §9.3.2](https://www.rfc-editor.org/rfc/rfc9110#section-9.3.2) 语义的实实现) | | 裸 @RequestMapping | 不声明 method 时 RequestMethodsRequestCondition 匹配任意Method | 在审计过程中,不能仅仅只静态比对"授权配置"和"Controller 注解"这两份代码的对应关系,正确的姿势是把 method 当作变量,沿着请求链路逐层对齐,例如权限拦截器保护的是什么、改写型过滤器把它变成什么、DispatcherServlet 最终按什么路由处理。任何不一致的地方,都有可能造成请求走私的风险。
发表于 2026-09-04 09:42:21
阅读 ( 2817 )
分类:
代码审计
3 推荐
收藏
1 条评论
tkswifty
73 篇文章
×
温馨提示
您当前没有「奇安信攻防社区」的账号,注册后可获取更多的使用权限。
×
温馨提示
您当前没有「奇安信攻防社区」的账号,注册后可获取更多的使用权限。
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!