Hello πŸ‘‹

The isCorsRequest utility classifies our same-origin requests as cross-origin because we use the Host header for routing in our internal infrastructure. The problem is that isCorsRequest compares the Host with the Origin header to determine this, and these will never be the same in our case. So, our same-origin requests are rejected by the server. This means that we have to specify the origins that our application is hosted on as allowed origins, and this quickly becomes a headache when you have to maintain this list across multiple micro-services that are hosted on multiple origins.

I am not aware of any specification that describes this behaviour. The fetch standard does not mention anything about comparing these headers to determine if it is a cross-origin request, and multiple sources claim that it is the browser (not the server) that should determine if a request should be rejected:

  • OWASP ... Based on the result of the OPTIONS request, the browser decides whether the request is allowed or not.
  • MDN ... allows a server to indicate any origins (domain, scheme, or port) other than its own from which a browser should permit loading resources.
  • Wikipedia ... the specification mandates that browsers "preflight" the request, soliciting supported methods from the server with an HTTP OPTIONS request method, and then, upon "approval" from the server, sending the actual request ...

I am aware of the ForwardedHeaderFilter, but I think this is a somewhat fragile solution to the problem because it relies heavily on the proxies to set these headers correctly. Also, we would probably have to whitelist the content of these headers to mitigate host header injection attacks.

Comment From: sdeleuze

So, our same-origin requests are rejected by the server.

Could you please provide a reproducer (server + a documented http or curl command line to use) to allow us to fully understand your use case and the code path exercised on Spring side?

Comment From: petterdaae

My endpoint:

@RestController
class HealthController {

    @GetMapping("/health")
    fun health(): String = "Healthy\n"
}

My spring security config:

@Configuration
@EnableWebSecurity
class WebSecurityConfig {

    @Bean
    fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
        http.invoke {
            authorizeHttpRequests {
                authorize(anyRequest, permitAll)
            }
            cors { }
        }
        return http.build()
    }

    @Bean
    fun corsConfigurationSource(): CorsConfigurationSource {
        val configuration = CorsConfiguration()
        configuration.allowedOrigins = listOf("http://example.com")
        configuration.allowedMethods = listOf("GET", "POST")
        val source = UrlBasedCorsConfigurationSource()
        source.registerCorsConfiguration("/**", configuration)
        return source
    }
}

When I run the below command, I expect my endpoint to reply normally, however, I get Invalid CORS request and 403 Forbidden.

curl http://localhost:8080/health -H"Origin: http://localhost:8080" -H"Host: internal.application.name.com"

(http://example.com is not relevant to mye question by the way, just wanted include a basic cors configuration)

Is this enough, or do you want me to create git repository with the full source code of the project?

Comment From: sdeleuze

I would appreciate a repo or attached source code if possible.

Comment From: petterdaae

https://github.com/petterdaae/spring-reproduce-cors

Comment From: sdeleuze

As usually with CORS, there are a lot of subtleties involved so I will try to summarize my understanding to check with you if it is correct, and share more context on our implementation choices.

CORS does not permit to identify for sure CORS requests just by using Origin and CORS headers (see the related spec section), so Spring is implementing CorsUtils#isCorsRequest method leveraging Host / Origin headers comparison. As far as I know, this is the only reasonable way to implement it, so it is unlikely we will change that because we need the CorsUtils#isCorsRequest logic is various places.

It is true than CORS specification does not specify that CORS requests should be rejected on server side as CORS protocol is designed to perform access control on client (user agent) side. But server-side frameworks can provide additional server-side processing to ensure an additional level of security. For example, some POST requests are considered as simple requests from a CORS point of view and sent directly without a preflight request. The implementation choice we made, for security reasons, is to not allow this kind of cross domain request, so in practice this kind of request not matching the CORS configuration is rejected (that means that the server-side processing is not done, preventing any unwanted side effect).

If I understand it right, handling your use case would mean give away this server-side security and allow some cross domain requests which could have side effects on server side, and just not add CORS response headers. I agree current implementation is an opinionated choice done for security reasons, but unless there are strong arguments for it, I am not in favor of reducing this security level for the use case you mentioned, especially given the fact there are workaround with ForwardedHeaderFilter.

Any thoughts?

Comment From: petterdaae

First, thank you for a thorough answer! Really appreciate it πŸ™Œ

I can totally see the benefits of blocking simple requests and that isCorsRequest probably is the most reasonable implementation in this case. I think my main concern with this behaviour is that people working with spring-security get a false impression of what cors really is, but this is of course just my opinion and I can for sure see the security benefits that you get! πŸ˜…

Do you think a solution could be to implement something like isSimpleRequest and only block the request if it satisfies isCorsRequest $$ isSimpleRequest? In our case, we mainly use application/json in the Content-Type header which would not qualify as a simple request - and hence, there is no security benefit of blocking it. This would of course make things more complex, so I am not sure what I think myself.

Comment From: rstoyanchev

@petterdaae are you looking to turn off CORS checks completely for your scenario? If so, you could plug in your own CorsProcessor to achieve that effect.

Comment From: petterdaae

@petterdaae are you looking to turn off CORS checks completely for your scenario? If so, you could plug in your own CorsProcessor to achieve that effect.

No, we have some cross-origin requests as well πŸ˜…

Comment From: rstoyanchev

So some requests originate from browsers and others from within the firewall then? Is it feasible to narrow down with URL patterns in which case you can configure a more open policy for some, see reference docs.

Comment From: petterdaae

Oh, so it would for example be possible to have CORS on /some-path/* and skip the CORS handling entirely on all other requests?

Comment From: rstoyanchev

Yes, it's possible to set up CORS handling centrally based on URL patterns. This can be layered on top of or instead of more fine-grained CORS configuration via annotations on controller methods.

Comment From: petterdaae

Ok, thanks! Did not think of that. I think this will solve most of our problems actually. However, it would still be a problem for endpoints that are both served from the same origin and a cross origin.

So if you agree with me that there could be some more logic around accepting requests that do not qualify as simple requests I would appreciate that! However, I can see that this decision has some drawbacks as well, so if you disagree with me, feel free to close the issue πŸ˜‡

Thanks for great feedback! πŸ™‡

Comment From: sdeleuze

The risk of reducing the security and introducing regressions seems too high to me, I prefer to decline this issue. Hopefully our feedback have helped. Thanks for your understanding.