jetty / jetty.project

Eclipse Jetty® - Web Container & Clients - supports HTTP/2, HTTP/1.1, HTTP/1.0, websocket, servlets, and more
https://eclipse.dev/jetty
Other
3.87k stars 1.91k forks source link

SameOrigin doen't work properly in Jetty 12 when enabling CORS. #12556

Open vv2020 opened 5 days ago

vv2020 commented 5 days ago

Jetty version(s) jetty 12.0.12

Jetty Environment ee10

Java version/vendor (use: java -version) java 17

OS type/version Ubuntu 20.04

Description We are migrating CORS validation from spring's cors filter to jetty cors (specifically using the jetty-cross-origin.xml file), after completing the migration, we encountered an issue with the /logout endpoint being unreachable.

While troubleshoot we initiate a post request from js to "/logout" endpoint and found jetty is blocking this request and replying with 400 " Origin not allowed".To clarify, our environment consist both js pages and application on same jetty server, so scheme://domain:port is same for all the request initiated from js to server, yet jetty blocks the request.

Further investigation revealed that Jetty appears to be blocking what seems like a same-origin request. For Example : when the Origin header is set to "http://localhost:8081" and the request is made to our application URL at "http://localhost:8081/sample/logout", Jetty is blocking the request.

We were wondering if this a expected behavior or a bug, If this is a expected behavior, we would appreciate it if you could provide documentation or any resources that explicitly mention this scenario.

How to reproduce? Initiate a request with Origin with the same url of application server.

sbordet commented 4 days ago

@vv2020, can you please detail whether all other same-origin requests to the server work, and it is only /logout that does not work?

What HTTP method is used for /logout? Just GET or POST?

If the CrossOriginHandler handles the request, it is because it has an Origin header, so likely is it not a GET request.

Unfortunately there is no specification for the server-side, so the server would have to match the Origin header with scheme+authority, and this may be complicated due to:

Allowing same-origin requests makes sense, it is the implementation that is delicate, and I'm worried to open up a security hole.

For example, Forwarded is not a forbidden header.

A script from evil.com can craft a request:

POST / HTTP/1.1
Host: good.com
Origin: evil.com
Forwarded: host=evil.com

The server could choose to compare Origin with Host, but would break legit proxies. Or compare Origin with Forwarded[host] and be exploited.

Makes sense?

@gregw thoughts?

vv2020 commented 4 days ago

@sbordet Yes, It is a post request and it blocks all the request not just those to /logout.

I would also be interested to know, what Jetty CORS would consider as same-origin ?

sbordet commented 4 days ago

@vv2020 it was always the case, even with the now deprecated CrossOriginFilter (replaced by CrossOriginHandler), that you had to be explicit also for same-origin in the configuration.

There was never a concept of "same-origin".

Comparing Origin with Host looks reasonable, though I’m not entirely clear on how this would break legitimate proxies.

The proxy receives a request with Host: good.com from the client, but then it forwards it towards one of the backends, so the request to the backend has Host: backend1:8080, which obviously would fail the comparison. But then the proxy would send Forwarded: host=good.com in the request to backend1.

vv2020 commented 4 days ago

@sbordet I hear you, but I don't see it will break legit proxies, From what I understand, the proposed behavior introduces an additional check for same-origin requests, but doesn't change the existing behavior regarding how the Origin header is validated.

For Example Consider below scenario : Client initiates a request with Host : good.com and with Origin : good.com now it intercepts by a proxy and changes to Host : by-proxy.com and origin as Origin : good.com.

Current Behaviour :

Jetty-cors checks whether it has Origin.

Proposed Behaviour :

Jetty-cors checks whether it has Origin.

So, based on this, it seems that we are not altering the existing behavior but simply enhancing it by allowing same-origin requests to pass through without blocking.

sbordet commented 4 days ago

@vv2020 in your proposed solution, where would Jetty retrieve "by-proxy.com" to compare with the value of the Origin header?

If Host header, HTTP/2 and HTTP/3 requests typically do not have the Host header at all (it could be synthesized with HostHeaderCustomizer though).

From your point of view of the application developer, you would need to configure differently your application, or Jetty, knowing in advance whether it will be deployed behind a proxy (in which case you need to configure CrossOriginHandler.allowedOriginPatterns=good.com), or not (in which case no configuration is necessary).

At this point, you just add CrossOriginHandler.allowedOriginPatterns=good.com and it will work in every case.

We will consider your proposal though: for simple deployment setups it may be a good default.

gregw commented 4 days ago

Typically, jetty will only inspect Forwarded headers if it has the ForwardedRequestCustomer enabled, which is typically only done when Jetty is behind a proxy. So either: a) Jetty is not behind a proxy, and the request is not customized, so the Forwarded headers are ignored and the request's authority will be good.com rather than bad.com. Thus the authority can be trusted; or b) Jetty us behind a proxy, and the request is customized, the Forwarded headers are inspected and the request's authority will be evil.com. HOWEVER, in this case, it is the responsibility of the proxy to not let through any bad/fake Forwarded headers. If the proxy is good AND the ForwardedRequestCustomizer is well configured, then the authority can be trusted.

Note that it is difficult to determine if jetty is behind a proxy, as the server can have multiple connectors, with different customizers. Thus on every request we'd have to get the connection, get the connector, find the HttpConnectionFactory, get the HttpConfiguration, get the customizers, look for the ForwardedRequestCustomizer. That's a bit of work and still does not tell us if the ForwardedRequestCustomizer is well configured or not (to accept exactly only the headers the proxy actually manages).

So it would be difficult to check the configuration to automatically determine if the authority can be trusted or not.

However, I do see the flip side, that it can be a pain to have to configure an explicit hostname in the CORS configuration.

Perhaps as a compromise, we add a configuration to the CORS handler that says setTrustAuthority(boolean), which is default set to false. But a user, who takes on the responsibility of carefully configuring the proxy setup can just set that to true, so they now trust the authority of the request, no matter where it comes from. This still requires a configuration step, but rather than hard coding a hostname, it is just a declaration that they have done the the work required to trust the hostname in the authority.