CSRF Attack via CORS Preflight Requests with Spring MVC or Spring WebFlux
Description
Spring Framework, versions 5.2.x prior to 5.2.3 are vulnerable to CSRF attacks through CORS preflight requests that target Spring MVC (spring-webmvc module) or Spring WebFlux (spring-webflux module) endpoints. Only non-authenticated endpoints are vulnerable because preflight requests should not include credentials and therefore requests should fail authentication. However a notable exception to this are Chrome based browsers when using client certificates for authentication since Chrome sends TLS client certificates in CORS preflight requests in violation of spec requirements. No HTTP body can be sent or received as a result of this attack.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Spring Framework 5.2.x prior to 5.2.3 is vulnerable to CSRF attacks via CORS preflight requests when using Chrome with client certificates.
Vulnerability
Overview
CVE-2020-5397 is a cross-site request forgery (CSRF) vulnerability in the Spring Framework, affecting versions 5.2.x prior to 5.2.3 [1]. The flaw exists in how Spring MVC and Spring WebFlux handle CORS preflight requests (HTTP OPTIONS). Under normal circumstances, preflight requests lack authentication credentials, which prevents CSRF. However, Chrome browsers violate the CORS specification by sending TLS client certificates in preflight requests, allowing an attacker to bypass authentication and perform unauthorized actions against unprotected endpoints [1].
Exploitation
Mechanism
The attack is executed by crafting a malicious web page that sends a CORS preflight request to a target Spring endpoint. Because Chrome includes TLS client certificates in these requests, the server may authenticate the request and process it as a valid session. The exploit does not allow sending or receiving HTTP body content, but it can trigger state-changing operations defined on the endpoint [1]. Only endpoints that do not require authentication under normal conditions are vulnerable, as the preflight request would otherwise be rejected [1].
Impact
An attacker can force a victim's browser to perform CSRF attacks on a Spring application that uses client certificate authentication, potentially executing unauthorized actions such as modifying data or performing privileged operations. The attacker must trick the victim into visiting a malicious site, and the target application must be configured to accept client certificates from Chrome [1].
Mitigation
Spring Framework 5.2.3 includes a fix that rejects preflight requests that contain authentication credentials, even if they include client certificates [2]. Users should upgrade to 5.2.3 or later to remediate this vulnerability. No workaround is provided for older versions [1].
AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.springframework:spring-webmvcMaven | >= 5.2.0, < 5.2.3 | 5.2.3 |
org.springframework:spring-webfluxMaven | >= 5.2.0, < 5.2.3 | 5.2.3 |
Affected products
3- ghsa-coords2 versions
>= 5.2.0, < 5.2.3+ 1 more
- (no CPE)range: >= 5.2.0, < 5.2.3
- (no CPE)range: >= 5.2.0, < 5.2.3
- Spring/Spring Frameworkv5Range: 5.2
Patches
1bc7d01048579Update CORS support
10 files changed · +74 −26
spring-webflux/src/main/java/org/springframework/web/reactive/handler/AbstractHandlerMapping.java+3 −3 modified@@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -182,8 +182,8 @@ public Mono<Object> getHandler(ServerWebExchange exchange) { if (logger.isDebugEnabled()) { logger.debug(exchange.getLogPrefix() + "Mapped to " + handler); } - if (hasCorsConfigurationSource(handler)) { - ServerHttpRequest request = exchange.getRequest(); + ServerHttpRequest request = exchange.getRequest(); + if (hasCorsConfigurationSource(handler) || CorsUtils.isPreFlightRequest(request)) { CorsConfiguration config = (this.corsConfigurationSource != null ? this.corsConfigurationSource.getCorsConfiguration(exchange) : null); CorsConfiguration handlerConfig = getCorsConfiguration(handler, exchange); config = (config != null ? config.combine(handlerConfig) : handlerConfig);
spring-webflux/src/main/java/org/springframework/web/reactive/result/method/AbstractHandlerMethodMapping.java+2 −3 modified@@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -374,8 +374,7 @@ protected HandlerMethod handleNoMatch(Set<T> mappings, ServerWebExchange exchang @Override protected boolean hasCorsConfigurationSource(Object handler) { return super.hasCorsConfigurationSource(handler) || - (handler instanceof HandlerMethod && this.mappingRegistry.getCorsConfiguration((HandlerMethod) handler) != null) || - handler.equals(PREFLIGHT_AMBIGUOUS_MATCH); + (handler instanceof HandlerMethod && this.mappingRegistry.getCorsConfiguration((HandlerMethod) handler) != null); } @Override
spring-webflux/src/test/java/org/springframework/web/reactive/handler/CorsUrlHandlerMappingTests.java+2 −2 modified@@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -70,7 +70,7 @@ public void preflightRequestWithoutCorsConfigurationProvider() throws Exception Object actual = this.handlerMapping.getHandler(exchange).block(); assertThat(actual).isNotNull(); - assertThat(actual).isSameAs(this.welcomeController); + assertThat(actual).isNotSameAs(this.welcomeController); } @Test
spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/CrossOriginAnnotationIntegrationTests.java+25 −1 modified@@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,11 +35,13 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.RestTemplate; import org.springframework.web.reactive.config.EnableWebFlux; import org.springframework.web.testfixture.http.server.reactive.bootstrap.HttpServer; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; /** * Integration tests with {@code @CrossOrigin} and {@code @RequestMapping} @@ -89,6 +91,28 @@ void actualGetRequestWithoutAnnotation(HttpServer httpServer) throws Exception { assertThat(entity.getBody()).isEqualTo("no"); } + @ParameterizedHttpServerTest + void optionsRequestWithAccessControlRequestMethod(HttpServer httpServer) throws Exception { + startServer(httpServer); + this.headers.clear(); + this.headers.add(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET"); + ResponseEntity<String> entity = performOptions("/no", this.headers, String.class); + assertThat(entity.getBody()).isNull(); + } + + @ParameterizedHttpServerTest + void preflightRequestWithoutAnnotation(HttpServer httpServer) throws Exception { + startServer(httpServer); + this.headers.add(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET"); + try { + performOptions("/no", this.headers, Void.class); + fail("Preflight request without CORS configuration should fail"); + } + catch (HttpClientErrorException ex) { + assertThat(ex.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); + } + } + @ParameterizedHttpServerTest void actualPostRequestWithoutAnnotation(HttpServer httpServer) throws Exception { startServer(httpServer);
spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMapping.java+2 −2 modified@@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -414,7 +414,7 @@ else if (logger.isDebugEnabled() && !request.getDispatcherType().equals(Dispatch logger.debug("Mapped to " + executionChain.getHandler()); } - if (hasCorsConfigurationSource(handler)) { + if (hasCorsConfigurationSource(handler) || CorsUtils.isPreFlightRequest(request)) { CorsConfiguration config = (this.corsConfigurationSource != null ? this.corsConfigurationSource.getCorsConfiguration(request) : null); CorsConfiguration handlerConfig = getCorsConfiguration(handler, request); config = (config != null ? config.combine(handlerConfig) : handlerConfig);
spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java+2 −3 modified@@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -458,8 +458,7 @@ protected HandlerMethod handleNoMatch(Set<T> mappings, String lookupPath, HttpSe @Override protected boolean hasCorsConfigurationSource(Object handler) { return super.hasCorsConfigurationSource(handler) || - (handler instanceof HandlerMethod && this.mappingRegistry.getCorsConfiguration((HandlerMethod) handler) != null) || - handler.equals(PREFLIGHT_AMBIGUOUS_MATCH); + (handler instanceof HandlerMethod && this.mappingRegistry.getCorsConfiguration((HandlerMethod) handler) != null); } @Override
spring-webmvc/src/test/java/org/springframework/web/servlet/handler/CorsAbstractHandlerMappingTests.java+3 −2 modified@@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -86,7 +86,8 @@ void preflightRequestWithoutCorsConfigurationProvider() throws Exception { HandlerExecutionChain chain = this.handlerMapping.getHandler(this.request); assertThat(chain).isNotNull(); - assertThat(chain.getHandler()).isInstanceOf(SimpleHandler.class); + assertThat(chain.getHandler()).isNotNull(); + assertThat(chain.getHandler().getClass().getSimpleName()).isEqualTo("PreFlightHandler"); } @Test
spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/CrossOriginTests.java+24 −1 modified@@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -69,6 +69,10 @@ public class CrossOriginTests { private final MockHttpServletRequest request = new MockHttpServletRequest(); + private final String optionsHandler = "org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping$HttpOptionsHandler#handle()"; + + private final String corsPreflightHandler = "org.springframework.web.servlet.handler.AbstractHandlerMapping$PreFlightHandler"; + @BeforeEach @SuppressWarnings("resource") @@ -96,6 +100,25 @@ public void noAnnotationWithoutOrigin() throws Exception { assertThat(getCorsConfiguration(chain, false)).isNull(); } + @Test + public void noAnnotationWithAccessControlRequestMethod() throws Exception { + this.handlerMapping.registerHandler(new MethodLevelController()); + MockHttpServletRequest request = new MockHttpServletRequest("OPTIONS", "/no"); + request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET"); + HandlerExecutionChain chain = this.handlerMapping.getHandler(request); + assertThat(chain.getHandler().toString()).isEqualTo(optionsHandler); + } + + @Test + public void noAnnotationWithPreflightRequest() throws Exception { + this.handlerMapping.registerHandler(new MethodLevelController()); + MockHttpServletRequest request = new MockHttpServletRequest("OPTIONS", "/no"); + request.addHeader(HttpHeaders.ORIGIN, "https://domain.com/"); + request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "GET"); + HandlerExecutionChain chain = this.handlerMapping.getHandler(request); + assertThat(chain.getHandler().getClass().getName()).isEqualTo(corsPreflightHandler); + } + @Test // SPR-12931 public void noAnnotationWithOrigin() throws Exception { this.handlerMapping.registerHandler(new MethodLevelController());
spring-web/src/main/java/org/springframework/web/cors/CorsUtils.java+4 −4 modified@@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -66,12 +66,12 @@ else if ("https".equals(scheme) || "wss".equals(scheme)) { } /** - * Returns {@code true} if the request is a valid CORS pre-flight one. - * To be used in combination with {@link #isCorsRequest(HttpServletRequest)} since - * regular CORS checks are not invoked here for performance reasons. + * Returns {@code true} if the request is a valid CORS pre-flight one by checking {code OPTIONS} method with + * {@code Origin} and {@code Access-Control-Request-Method} headers presence. */ public static boolean isPreFlightRequest(HttpServletRequest request) { return (HttpMethod.OPTIONS.matches(request.getMethod()) && + request.getHeader(HttpHeaders.ORIGIN) != null && request.getHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD) != null); }
spring-web/src/main/java/org/springframework/web/cors/reactive/CorsUtils.java+7 −5 modified@@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -45,12 +45,14 @@ public static boolean isCorsRequest(ServerHttpRequest request) { } /** - * Returns {@code true} if the request is a valid CORS pre-flight one. - * To be used in combination with {@link #isCorsRequest(ServerHttpRequest)} since - * regular CORS checks are not invoked here for performance reasons. + * Returns {@code true} if the request is a valid CORS pre-flight one by checking {code OPTIONS} method with + * {@code Origin} and {@code Access-Control-Request-Method} headers presence. */ public static boolean isPreFlightRequest(ServerHttpRequest request) { - return (request.getMethod() == HttpMethod.OPTIONS && request.getHeaders().containsKey(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD)); + HttpHeaders headers = request.getHeaders(); + return (request.getMethod() == HttpMethod.OPTIONS + && headers.containsKey(HttpHeaders.ORIGIN) + && headers.containsKey(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD)); } /**
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
10- github.com/advisories/GHSA-7pm4-g2qj-j85xghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2020-5397ghsaADVISORY
- github.com/spring-projects/spring-framework/commit/bc7d01048579430b4b2df668178809b63d3f1929ghsaWEB
- pivotal.io/security/cve-2020-5397ghsax_refsource_CONFIRMWEB
- www.oracle.com//security-alerts/cpujul2021.htmlghsax_refsource_MISCWEB
- www.oracle.com/security-alerts/cpuapr2020.htmlghsax_refsource_MISCWEB
- www.oracle.com/security-alerts/cpujul2020.htmlghsax_refsource_MISCWEB
- www.oracle.com/security-alerts/cpujul2022.htmlghsax_refsource_MISCWEB
- www.oracle.com/security-alerts/cpuoct2020.htmlghsax_refsource_MISCWEB
- www.oracle.com/security-alerts/cpuoct2021.htmlghsax_refsource_MISCWEB
News mentions
0No linked articles in our index yet.