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.