Skip to content

Consider not rejecting preflight requests when no CORS configuration is provided #31839

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
rcolombo opened this issue Dec 14, 2023 · 8 comments
Closed
Assignees
Labels
in: web Issues in web modules (web, webmvc, webflux, websocket) type: enhancement A general enhancement
Milestone

Comments

@rcolombo
Copy link

rcolombo commented Dec 14, 2023

Affects: 3.2.0


My project is now getting "403 Invalid CORS Request" responses when sending an OPTIONS request with the "access-control-request-method" header. It worked fine in version 3.1.6.

"access-control-request-method" is a valid CORS header so I'm not sure why this error is surfacing. Two other CORS headers that I use are "origin" and "access-control-request-headers" and they cause no issues. I can send any combination of those two headers and I get the correct responses.

One way around this is to add a custom CorsMapping like such:

@EnableWebMvc
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

  @Override
  public void addCorsMappings(CorsRegistry registry) {
    registry.addMapping("/**")
        .allowedMethods("*")
        .allowedOrigins("*")
        .allowedHeaders("*");
  }

However, I don't want to do this as I have my own custom CORSInterceptor that I want to handle OPTIONS requests. The above solution does things I don't want, such as setting the response header of access-control-allow-origin: "*"

Did something change that specifically causes issues with just this header?

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Dec 14, 2023
@rcolombo rcolombo reopened this Dec 14, 2023
@bclozel
Copy link
Member

bclozel commented Dec 14, 2023

Hello @rcolombo - unfortunately, there isn't enough information here to help you. I'm not seeing any specific CORS-related change in the 6.1.x line.

Can you given an example of request that is being rejected? You're also mentioning a custom CORS interceptor and we're not seeing it here. Can you share a minimal sample application that reproduces the problem?

@bclozel bclozel added status: waiting-for-feedback We need additional information before we can continue in: web Issues in web modules (web, webmvc, webflux, websocket) labels Dec 14, 2023
@rstoyanchev
Copy link
Contributor

rstoyanchev commented Dec 14, 2023

You could place a debug point in DefaultCorsProcessor, or otherwise provide a minimal sample.

@rcolombo
Copy link
Author

rcolombo commented Dec 14, 2023

Perhaps this is user error. Thank you for pointing me to the DefaultCorsProcessor.

This line rejects my request because I have a preflight request but a null config: https://github.com/spring-projects/spring-framework/blob/main/spring-web/src/main/java/org/springframework/web/cors/DefaultCorsProcessor.java#L85

I'm still unclear why I didn't see this until version 3.2.0 as the DefaultCorsProcessor has been stable for some time. Apologies if this was improperly filed as an issue - there must be something else in my setup that has this working for 3.1.6 and not 3.2.0.

The fix that worked for my exact use case was to implement my CORS Interceptor as a CORS Filter instead. This way, I am able to add CORS response headers before DefaultCorsProcessor processes the request (effectively neutering that processor).

My overall goal is to skip/disable the built-in Spring CORS Processor, as my exact use case requires more complicated processing.

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels Dec 14, 2023
@sdeleuze
Copy link
Contributor

Based on your feedback, I close this issue.

If you want to skip Spring processing, putting a custom CORS filter will indeed likely disable Spring own CORS processing if it sets CORS response headers.

Be aware you also could probably instead provide your own CorsProcessor via org.springframework.web.servlet.handler.AbstractHandlerMapping#setCorsProcessor.

@sdeleuze sdeleuze closed this as not planned Won't fix, can't repro, duplicate, stale Dec 15, 2023
@sdeleuze sdeleuze added status: invalid An issue that we don't feel is valid and removed status: waiting-for-triage An issue we've not yet triaged or decided on labels Dec 15, 2023
@kenips
Copy link

kenips commented Sep 20, 2024

@sdeleuze @rcolombo this is a valid issue even when original poster decided to move away from built-in processor.

The issue stems from https://github.com/spring-projects/spring-framework/blob/main/spring-web/src/main/java/org/springframework/web/cors/DefaultCorsProcessor.java#L96-104:

if (config == null) {
	if (preFlightRequest) {
		rejectRequest(new ServletServerHttpResponse(response));
		return false;
	}
	else {
		return true;
	}
}

where when one does not supply the config, it is assumed that CORS is not enabled and hence you return true. However the block above create a situation where you reject the pre-flight even when config is null. You end up having users reporting issues that only a combination of headers in pre-flight would trigger this, like this here: spring-cloud/spring-cloud-gateway#112.

For my use case, I'd like pre-flight AND actual calls to all route to target server in Spring Cloud Gateway, and the expectation is that I can disable pre-flight rejection here as well. When I start supplying a config to make this work, I ended up having CORS check both at my gateway AND my target app, resulting duplicated Access-Control-Allow-Origin on my final response.

As it currently stands, I also end up rolling my own CorsFilter to allow pre-flight at gateway only, and letting target servers to handle actual.

@workflo
Copy link

workflo commented Mar 13, 2025

I had a very similar problem: I'm migrating a huge legacy app (20+ years of development ;) ) to using Spring Web bit by bit.
As we already have custom code that handles OPTIONS and especially preflight requests I don't want to interfere Spring in handling them AT ALL.

I ended up implementing a ServletFilter to kind of hide preflight requests from Spring by wrapping the respective request and disguising it as a simple GET to avoid any of Spring Web's logic to trigger.
Afterwards I unwrap the request in the respective Controller...

web.xml:

  <filter> 
    <filter-name>hideOptionRequestFromSpringFilter</filter-name>
    <filter-class>HideOptionRequestFromSpringFilter</filter-class> 
  </filter> 
  <filter-mapping> 
    <filter-name>hideOptionRequestFromSpringFilter</filter-name>
    <url-pattern>/rest/*</url-pattern> 
  </filter-mapping>

HideOptionRequestFromSpringFilter.java:

import java.io.IOException;

import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;

import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;


/**
 * Hides the Origin header from Spring to skip its CORS processing.
 */
public class HideOptionRequestFromSpringFilter implements Filter {
    public final static String HIDE_ORIGIN_FROM_SPRING_ATTRIBUTE = "xxx.SkipSpringCorsProcessorFilter.Origin";
    public final static String HIDE_METHOD_FROM_SPRING_ATTRIBUTE = "xxx.SkipSpringCorsProcessorFilter.Method";
    
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        UserBean.detachFromCurrentThread();
        
        final HttpServletRequest httpRequest = (HttpServletRequest) request;

        if (HttpMethod.OPTIONS.matches(httpRequest.getMethod())) {
            httpRequest.setAttribute(HIDE_ORIGIN_FROM_SPRING_ATTRIBUTE, httpRequest.getHeader(HttpHeaders.ORIGIN));
            httpRequest.setAttribute(HIDE_METHOD_FROM_SPRING_ATTRIBUTE, httpRequest.getMethod());
            
            HttpServletRequestWrapper wrapper = new HttpServletRequestWrapper(httpRequest) {
                @Override
                public String getHeader(String name) {
                    if (HttpHeaders.ORIGIN.equalsIgnoreCase(name)) {
                        return null;
                    }
                    return super.getHeader(name);
                }
                
                @Override
                public String getMethod() {
                    return "GET";
                }
            };
                        
            chain.doFilter(wrapper, response);
        } else {
            chain.doFilter(request, response);
        }
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {}
    
    @Override
    public void destroy() {}
}

RestService.java:

@RestController
public class RestService {
    /**
     * Handle multi-part requests.
     */
    @RequestMapping(path = "/rest/**")
    protected void service(
        HttpServletRequest request, 
        HttpServletResponse response
    ) throws ServletException, IOException {
        // Unwrap original Request; Counterpart to HideOptionRequestFromSpringFilter
        if (request instanceof HttpServletRequestWrapper) {
            final HttpServletRequestWrapper w = (HttpServletRequestWrapper) request;
            final HttpServletRequest wrappedRequest = (HttpServletRequest) w.getRequest();
            
            if (HttpMethod.GET.matches(request.getMethod()) && HttpMethod.OPTIONS.matches(wrappedRequest.getMethod())) {
                request = wrappedRequest;
            }
        }
        
        // Handle request in our very own way...
    }

Maybe in a next step we'll try to adopt Spring Web's handling of preflight requests into our custom (and scriptable) way of handling them. Won't be an easy task, thou.

@sdeleuze sdeleuze self-assigned this Mar 20, 2025
@sdeleuze
Copy link
Contributor

sdeleuze commented Mar 20, 2025

Maybe rejecting preflight requests when there is no CORS config is indeed too strong, as the lack of proper response header could be enough to get the right default behavior, and it could improve consistency between preflight/non-preflight request and help non-builtin CORS support like the use cases described above. I will tentatively see if I can provide a reasonable solution for 6.2.x (otherwise, I may target 7.0.x).

@sdeleuze sdeleuze reopened this Mar 20, 2025
@sdeleuze sdeleuze added type: enhancement A general enhancement and removed status: invalid An issue that we don't feel is valid status: feedback-provided Feedback has been provided labels Mar 20, 2025
@sdeleuze sdeleuze added this to the 6.2.6 milestone Mar 20, 2025
@sdeleuze sdeleuze changed the title [3.2.0] OPTIONS requests failing with "403 Invalid CORS Request" with presence of "access-control-request-method" header. Consider not rejecting preflight requests when no CORS configuration is provided Mar 20, 2025
@sdeleuze sdeleuze modified the milestones: 6.2.6, 7.0.0-M4 Apr 3, 2025
@sdeleuze
Copy link
Contributor

sdeleuze commented Apr 3, 2025

After discussing with @rstoyanchev, we decided to target Spring Framework 7.0 as there is a workaround and we prefer to get early feedback on this change, even if in theory that should be fine.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: web Issues in web modules (web, webmvc, webflux, websocket) type: enhancement A general enhancement
Projects
None yet
Development

No branches or pull requests

7 participants