VYPR
Moderate severityNVD Advisory· Published Jan 17, 2020· Updated Sep 17, 2024

CSRF Attack via CORS Preflight Requests with Spring MVC or Spring WebFlux

CVE-2020-5397

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.

PackageAffected versionsPatched versions
org.springframework:spring-webmvcMaven
>= 5.2.0, < 5.2.35.2.3
org.springframework:spring-webfluxMaven
>= 5.2.0, < 5.2.35.2.3

Affected products

3

Patches

1
bc7d01048579

Update CORS support

https://github.com/spring-projects/spring-frameworkSébastien DeleuzeJan 10, 2020via ghsa
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

News mentions

0

No linked articles in our index yet.