diff --git a/documentation/jetty-documentation/src/main/asciidoc/operations-guide/modules/module-cross-origin.adoc b/documentation/jetty-documentation/src/main/asciidoc/operations-guide/modules/module-cross-origin.adoc index 8bde6ba208b..d67dbce40c4 100644 --- a/documentation/jetty-documentation/src/main/asciidoc/operations-guide/modules/module-cross-origin.adoc +++ b/documentation/jetty-documentation/src/main/asciidoc/operations-guide/modules/module-cross-origin.adoc @@ -18,10 +18,16 @@ The `cross-origin` module provides support for the link:https://developer.mozill This module installs the xref:{prog-guide}#pg-server-http-handler-use-cross-origin[`CrossOriginHandler`] in the `Handler` tree; `CrossOriginHandler` inspects cross-origin requests and adds the relevant CORS response headers. -`CrossOriginHandler` should be used when an application performs cross-origin requests to your server, to protect from link:https://owasp.org/www-community/attacks/csrf[cross-site request forgery] attacks. +`CrossOriginHandler` should be used when an application performs cross-origin requests to your domain, to protect from link:https://owasp.org/www-community/attacks/csrf[cross-site request forgery] attacks. The module properties are: ---- include::{jetty-home}/modules/cross-origin.mod[tags=documentation] ---- + +You must configure at least the property `jetty.crossorigin.allowedOriginPatterns` to allow one or more origins. + +It is recommended that you consider configuring also the property `jetty.crossorigin.allowCredentials`. +When set to `true`, clients send cookies and authentication headers in cross-origin requests to your domain. +When set to `false`, cookies and authentication headers are not sent. diff --git a/documentation/jetty-documentation/src/main/asciidoc/programming-guide/server/http/server-http-handler-use.adoc b/documentation/jetty-documentation/src/main/asciidoc/programming-guide/server/http/server-http-handler-use.adoc index 0a1213ddbeb..7c0cd8c0361 100644 --- a/documentation/jetty-documentation/src/main/asciidoc/programming-guide/server/http/server-http-handler-use.adoc +++ b/documentation/jetty-documentation/src/main/asciidoc/programming-guide/server/http/server-http-handler-use.adoc @@ -413,7 +413,7 @@ An example of a cross-origin request is when a script downloaded from the origin This is common, for example, when you embed reusable components such as a chat component into a web page: the web page and the chat component files are downloaded from `+http://domain.com+`, but the chat server is at `+http://chat.domain.com+`, so the chat component must make cross-origin requests to the chat server. -This kind of setup exposes to link:https://owasp.org/www-community/attacks/csrf[cross-site request forgery attacks], and the CORS protocol has been established to protect against this kind of attacks. +This kind of setup exposes to link:https://owasp.org/www-community/attacks/csrf[cross-site request forgery (CSRF) attacks], and the CORS protocol has been established to protect against this kind of attacks. For security reasons, browsers by default do not allow cross-origin requests, unless the response from the cross domain contains the right CORS headers. @@ -430,9 +430,9 @@ Server └── AppHandler ---- -The most important `CrossOriginHandler` configuration parameter is `allowedOrigins`, which by default is `*`, allowing any origin. +The most important `CrossOriginHandler` configuration parameter that must be configured is `allowedOrigins`, which by default is the empty set, therefore disallowing all origins. -You may want to restrict your server to only origins you trust. +You want to restrict requests to your cross domain to only origins you trust. From the chat example above, the chat server at `+http://chat.domain.com+` knows that the chat component is downloaded from the origin server at `+http://domain.com+`, so the `CrossOriginHandler` is configured in this way: [source,java,indent=0] @@ -440,7 +440,7 @@ From the chat example above, the chat server at `+http://chat.domain.com+` knows include::../../{doc_code}/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=crossOriginAllowedOrigins] ---- -Browsers send cross-origin request in two ways: +Browsers send cross-origin requests in two ways: * Directly, if the cross-origin request meets some simple criteria. * By issuing a hidden _preflight_ request before the actual cross-origin request, to verify with the server if it is willing to reply properly to the actual cross-origin request. @@ -449,6 +449,11 @@ Both preflight requests and cross-origin requests will be handled by `CrossOrigi By default, preflight requests are not delivered to the `CrossOriginHandler` child `Handler`, but it is possible to configure `CrossOriginHandler` by setting `deliverPreflightRequests=true` so that the web application can fine-tune the CORS response headers. +Another important `CrossOriginHandler` configuration parameter is `allowCredentials`, which controls whether cookies and authentication headers that match the cross-origin request to the cross domain are sent in the cross-origin requests. +By default, `allowCredentials=false` so that cookies and authentication headers are not sent in cross-origin requests. + +If the application deployed in the cross domain requires cookies or authentication, then you must set `allowCredentials=true`, but you also need to restrict the allowed origins only to the ones your trust, otherwise your cross domain application will be vulnerable to CSRF attacks. + For more `CrossOriginHandler` configuration options, refer to the link:{javadoc-url}/org/eclipse/jetty/server/handler/CrossOriginHandler.html[`CrossOriginHandler` javadocs]. [[pg-server-http-handler-use-default]] diff --git a/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java b/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java index 1572e0ceb68..25a7e378fb9 100644 --- a/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java +++ b/documentation/jetty-documentation/src/main/java/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java @@ -1134,6 +1134,8 @@ public class HTTPServerDocs // Add the CrossOriginHandler to protect from CSRF attacks. CrossOriginHandler crossOriginHandler = new CrossOriginHandler(); + crossOriginHandler.setAllowedOriginPatterns(Set.of("http://domain.com")); + crossOriginHandler.setAllowCredentials(true); server.setHandler(crossOriginHandler); // Create a ServletContextHandler with contextPath. diff --git a/jetty-core/jetty-server/src/main/config/etc/jetty-cross-origin.xml b/jetty-core/jetty-server/src/main/config/etc/jetty-cross-origin.xml index 7bc59d95115..3d6ee7d072b 100644 --- a/jetty-core/jetty-server/src/main/config/etc/jetty-cross-origin.xml +++ b/jetty-core/jetty-server/src/main/config/etc/jetty-cross-origin.xml @@ -5,9 +5,7 @@ - - - + @@ -30,7 +28,7 @@ - + diff --git a/jetty-core/jetty-server/src/main/config/modules/cross-origin.mod b/jetty-core/jetty-server/src/main/config/modules/cross-origin.mod index 24d6356a6c7..c9bf176b41f 100644 --- a/jetty-core/jetty-server/src/main/config/modules/cross-origin.mod +++ b/jetty-core/jetty-server/src/main/config/modules/cross-origin.mod @@ -17,7 +17,7 @@ etc/jetty-cross-origin.xml [ini-template] #tag::documentation[] ## Whether cross-origin requests can include credentials such as cookies or authentication headers. -# jetty.crossorigin.allowCredentials=true +# jetty.crossorigin.allowCredentials=false ## A comma-separated list of headers allowed in cross-origin requests. # jetty.crossorigin.allowedHeaders=Content-Type @@ -26,7 +26,7 @@ etc/jetty-cross-origin.xml # jetty.crossorigin.allowedMethods=GET,POST,HEAD ## A comma-separated list of origins regex patterns allowed in cross-origin requests. -# jetty.crossorigin.allowedOriginPatterns=* +# jetty.crossorigin.allowedOriginPatterns= ## A comma-separated list of timing origins regex patterns allowed in cross-origin requests. # jetty.crossorigin.allowedTimingOriginPatterns= diff --git a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/CrossOriginHandler.java b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/CrossOriginHandler.java index e5e9a703deb..74e2b1011c3 100644 --- a/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/CrossOriginHandler.java +++ b/jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/handler/CrossOriginHandler.java @@ -47,9 +47,13 @@ import org.slf4j.LoggerFactory; * Origin: http://domain.com * } *

The cross server at {@code cross.domain.com} must decide whether these cross-origin requests - * are allowed or not, and it may easily do so by configuring the {@link CrossOriginHandler}, - * for example configuring the {@link #setAllowedOriginPatterns(Set) allowed origins} to contain only + * are allowed or not, by configuring the {@link CrossOriginHandler} + * {@link #setAllowedOriginPatterns(Set) allowed origins} to contain only * the origin server with origin {@code http://domain.com}.

+ *

The cross server must also decide whether cross-origin requests are allowed to contain + * credentials (cookies and authentication headers) or not, by configuring + * {@link #setAllowCredentials(boolean)}.

+ *

By default, no origin is allowed, and credentials are not allowed.

*/ @ManagedObject public class CrossOriginHandler extends Handler.Wrapper @@ -58,10 +62,10 @@ public class CrossOriginHandler extends Handler.Wrapper private static final PreEncodedHttpField ACCESS_CONTROL_ALLOW_CREDENTIALS_TRUE = new PreEncodedHttpField(HttpHeader.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); private static final PreEncodedHttpField VARY_ORIGIN = new PreEncodedHttpField(HttpHeader.VARY, HttpHeader.ORIGIN.asString()); - private boolean allowCredentials = true; + private boolean allowCredentials = false; private Set allowedHeaders = Set.of("Content-Type"); private Set allowedMethods = Set.of("GET", "POST", "HEAD"); - private Set allowedOrigins = Set.of("*"); + private Set allowedOrigins = Set.of(); private Set allowedTimingOrigins = Set.of(); private boolean deliverPreflight = false; private boolean deliverNonAllowedOrigin = true; @@ -314,6 +318,10 @@ public class CrossOriginHandler extends Handler.Wrapper accessControlAllowHeadersField = new PreEncodedHttpField(HttpHeader.ACCESS_CONTROL_ALLOW_HEADERS, String.join(",", getAllowedHeaders())); accessControlExposeHeadersField = new PreEncodedHttpField(HttpHeader.ACCESS_CONTROL_EXPOSE_HEADERS, String.join(",", getExposedHeaders())); accessControlMaxAge = new PreEncodedHttpField(HttpHeader.ACCESS_CONTROL_MAX_AGE, getPreflightMaxAge().toSeconds()); + + if (anyOriginAllowed && isAllowCredentials()) + LOG.warn("{} configured with insecure parameters allowedOrigins=* and allowCredentials=true", getClass().getSimpleName()); + super.doStart(); } diff --git a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/CrossOriginHandlerTest.java b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/CrossOriginHandlerTest.java index e641b67b36c..0f674ae10dd 100644 --- a/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/CrossOriginHandlerTest.java +++ b/jetty-core/jetty-server/src/test/java/org/eclipse/jetty/server/handler/CrossOriginHandlerTest.java @@ -124,7 +124,9 @@ public class CrossOriginHandlerTest public void testSimpleRequestWithWildcardOrigin() throws Exception { String origin = "http://foo.example.com"; - start(new CrossOriginHandler()); + CrossOriginHandler crossOriginHandler = new CrossOriginHandler(); + crossOriginHandler.setAllowedOriginPatterns(Set.of("*")); + start(crossOriginHandler); String request = """ GET / HTTP/1.1\r @@ -138,7 +140,7 @@ public class CrossOriginHandlerTest assertThat(response.getStatus(), is(HttpStatus.OK_200)); assertTrue(response.contains(ApplicationHandler.APPLICATION_HEADER)); assertTrue(response.contains(HttpHeader.ACCESS_CONTROL_ALLOW_ORIGIN)); - assertTrue(response.contains(HttpHeader.ACCESS_CONTROL_ALLOW_CREDENTIALS)); + assertFalse(response.contains(HttpHeader.ACCESS_CONTROL_ALLOW_CREDENTIALS)); assertTrue(response.contains(HttpHeader.VARY)); } @@ -148,6 +150,7 @@ public class CrossOriginHandlerTest String origin = "http://subdomain.example.com"; CrossOriginHandler crossOriginHandler = new CrossOriginHandler(); crossOriginHandler.setAllowedOriginPatterns(Set.of("http://.*\\.example\\.com")); + crossOriginHandler.setAllowCredentials(true); start(crossOriginHandler); String request = """ @@ -186,7 +189,7 @@ public class CrossOriginHandlerTest assertThat(response.getStatus(), is(HttpStatus.OK_200)); assertTrue(response.contains(ApplicationHandler.APPLICATION_HEADER)); assertTrue(response.contains(HttpHeader.ACCESS_CONTROL_ALLOW_ORIGIN)); - assertTrue(response.contains(HttpHeader.ACCESS_CONTROL_ALLOW_CREDENTIALS)); + assertFalse(response.contains(HttpHeader.ACCESS_CONTROL_ALLOW_CREDENTIALS)); assertTrue(response.contains(HttpHeader.VARY)); } @@ -196,6 +199,7 @@ public class CrossOriginHandlerTest String origin = "http://localhost"; CrossOriginHandler crossOriginHandler = new CrossOriginHandler(); crossOriginHandler.setAllowedOriginPatterns(Set.of(origin)); + crossOriginHandler.setAllowCredentials(true); start(crossOriginHandler); String request = """ @@ -237,7 +241,7 @@ public class CrossOriginHandlerTest assertThat(response.getStatus(), is(HttpStatus.OK_200)); assertTrue(response.contains(ApplicationHandler.APPLICATION_HEADER)); assertTrue(response.contains(HttpHeader.ACCESS_CONTROL_ALLOW_ORIGIN)); - assertTrue(response.contains(HttpHeader.ACCESS_CONTROL_ALLOW_CREDENTIALS)); + assertFalse(response.contains(HttpHeader.ACCESS_CONTROL_ALLOW_CREDENTIALS)); assertFalse(response.contains(HttpHeader.TIMING_ALLOW_ORIGIN)); assertTrue(response.contains(HttpHeader.VARY)); } @@ -249,6 +253,7 @@ public class CrossOriginHandlerTest CrossOriginHandler crossOriginHandler = new CrossOriginHandler(); crossOriginHandler.setAllowedOriginPatterns(Set.of(origin)); crossOriginHandler.setAllowedTimingOriginPatterns(Set.of(origin)); + crossOriginHandler.setAllowCredentials(true); start(crossOriginHandler); String request = """ @@ -275,6 +280,7 @@ public class CrossOriginHandlerTest String otherOrigin = "http://127\\.0\\.0\\.1"; CrossOriginHandler crossOriginHandler = new CrossOriginHandler(); crossOriginHandler.setAllowedOriginPatterns(Set.of(origin, otherOrigin)); + crossOriginHandler.setAllowCredentials(true); start(crossOriginHandler); // Use 2 spaces as separator in the Origin header @@ -298,7 +304,9 @@ public class CrossOriginHandlerTest @Test public void testSimpleRequestWithoutCredentials() throws Exception { + String origin = "http://localhost"; CrossOriginHandler crossOriginHandler = new CrossOriginHandler(); + crossOriginHandler.setAllowedOriginPatterns(Set.of(origin)); crossOriginHandler.setAllowCredentials(false); start(crossOriginHandler); @@ -306,9 +314,9 @@ public class CrossOriginHandlerTest GET / HTTP/1.1\r Host: localhost\r Connection: close\r - Origin: http://localhost\r + Origin: %s\r \r - """; + """.formatted(origin); HttpTester.Response response = HttpTester.parseResponse(connector.getResponse(request)); assertThat(response.getStatus(), is(HttpStatus.OK_200)); @@ -324,7 +332,10 @@ public class CrossOriginHandlerTest // we'll trust browsers to do it right, so responses to actual requests // will contain the CORS response headers. - start(new CrossOriginHandler()); + CrossOriginHandler crossOriginHandler = new CrossOriginHandler(); + crossOriginHandler.setAllowedOriginPatterns(Set.of("*")); + crossOriginHandler.setAllowCredentials(true); + start(crossOriginHandler); String request = """ PUT / HTTP/1.1\r @@ -345,18 +356,22 @@ public class CrossOriginHandlerTest public void testOptionsRequestButNotPreflight() throws Exception { // We cannot know if an actual request has performed the preflight before: - // we'll trust browsers to do it right, so responses to actual requests + // we'll trust browsers to do it right, so responses to OPTIONS requests // will contain the CORS response headers. - start(new CrossOriginHandler()); + String origin = "http://localhost"; + CrossOriginHandler crossOriginHandler = new CrossOriginHandler(); + crossOriginHandler.setAllowedOriginPatterns(Set.of(origin)); + crossOriginHandler.setAllowCredentials(true); + start(crossOriginHandler); String request = """ OPTIONS / HTTP/1.1\r Host: localhost\r Connection: close\r - Origin: http://localhost\r + Origin: %s\r \r - """; + """.formatted(origin); HttpTester.Response response = HttpTester.parseResponse(connector.getResponse(request)); assertThat(response.getStatus(), is(HttpStatus.OK_200)); @@ -369,6 +384,8 @@ public class CrossOriginHandlerTest public void testPreflightWithWildcardCustomHeaders() throws Exception { CrossOriginHandler crossOriginHandler = new CrossOriginHandler(); + crossOriginHandler.setAllowedOriginPatterns(Set.of("*")); + crossOriginHandler.setAllowCredentials(true); crossOriginHandler.setAllowedHeaders(Set.of("*")); start(crossOriginHandler); @@ -393,7 +410,10 @@ public class CrossOriginHandlerTest @Test public void testPUTRequestWithPreflight() throws Exception { + String origin = "http://localhost"; CrossOriginHandler crossOriginHandler = new CrossOriginHandler(); + crossOriginHandler.setAllowedOriginPatterns(Set.of(origin)); + crossOriginHandler.setAllowCredentials(true); crossOriginHandler.setAllowedMethods(Set.of("PUT")); start(crossOriginHandler); @@ -403,9 +423,9 @@ public class CrossOriginHandlerTest Host: localhost\r Connection: close\r Access-Control-Request-Method: PUT\r - Origin: http://localhost\r + Origin: %s\r \r - """; + """.formatted(origin); HttpTester.Response response = HttpTester.parseResponse(connector.getResponse(request)); assertThat(response.getStatus(), is(HttpStatus.OK_200)); @@ -435,9 +455,12 @@ public class CrossOriginHandlerTest @Test public void testDELETERequestWithPreflightAndAllowedCustomHeaders() throws Exception { + String origin = "http://localhost"; CrossOriginHandler crossOriginHandler = new CrossOriginHandler(); + crossOriginHandler.setAllowedOriginPatterns(Set.of(origin)); crossOriginHandler.setAllowedMethods(Set.of("GET", "HEAD", "POST", "PUT", "DELETE")); crossOriginHandler.setAllowedHeaders(Set.of("X-Requested-With")); + crossOriginHandler.setAllowCredentials(true); start(crossOriginHandler); // Preflight request. @@ -447,9 +470,9 @@ public class CrossOriginHandlerTest Connection: close\r Access-Control-Request-Method: DELETE\r Access-Control-Request-Headers: origin,x-custom,x-requested-with\r - Origin: http://localhost\r + Origin: %s\r \r - """; + """.formatted(origin); HttpTester.Response response = HttpTester.parseResponse(connector.getResponse(request)); assertThat(response.getStatus(), is(HttpStatus.OK_200)); @@ -467,9 +490,9 @@ public class CrossOriginHandlerTest Connection: close\r X-Custom: value\r X-Requested-With: local\r - Origin: http://localhost\r + Origin: %s\r \r - """; + """.formatted(origin); response = HttpTester.parseResponse(connector.getResponse(request)); assertThat(response.getStatus(), is(HttpStatus.OK_200)); @@ -482,6 +505,7 @@ public class CrossOriginHandlerTest public void testDELETERequestWithPreflightAndNotAllowedCustomHeaders() throws Exception { CrossOriginHandler crossOriginHandler = new CrossOriginHandler(); + crossOriginHandler.setAllowedOriginPatterns(Set.of("*")); crossOriginHandler.setAllowedMethods(Set.of("GET", "HEAD", "POST", "PUT", "DELETE")); start(crossOriginHandler); @@ -508,7 +532,9 @@ public class CrossOriginHandlerTest @Test public void testSimpleRequestWithExposedHeaders() throws Exception { + String origin = "http://localhost"; CrossOriginHandler crossOriginHandler = new CrossOriginHandler(); + crossOriginHandler.setAllowedOriginPatterns(Set.of(origin)); crossOriginHandler.setExposedHeaders(Set.of("Content-Length")); start(crossOriginHandler); @@ -516,9 +542,9 @@ public class CrossOriginHandlerTest GET / HTTP/1.1\r Host: localhost\r Connection: close\r - Origin: http://localhost\r + Origin: %s\r \r - """; + """.formatted(origin); HttpTester.Response response = HttpTester.parseResponse(connector.getResponse(request)); assertThat(response.getStatus(), is(HttpStatus.OK_200)); @@ -530,6 +556,7 @@ public class CrossOriginHandlerTest public void testDoNotDeliverPreflightRequest() throws Exception { CrossOriginHandler crossOriginHandler = new CrossOriginHandler(); + crossOriginHandler.setAllowedOriginPatterns(Set.of("*")); crossOriginHandler.setDeliverPreflightRequests(false); start(crossOriginHandler); @@ -553,6 +580,8 @@ public class CrossOriginHandlerTest public void testDeliverWebSocketUpgradeRequest() throws Exception { CrossOriginHandler crossOriginHandler = new CrossOriginHandler(); + crossOriginHandler.setAllowedOriginPatterns(Set.of("*")); + crossOriginHandler.setAllowCredentials(true); start(crossOriginHandler); // Preflight request. diff --git a/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java b/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java index d477b1c6f8a..d7ceee87117 100644 --- a/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java +++ b/tests/test-distribution/test-distribution-common/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java @@ -1817,13 +1817,19 @@ public class DistributionTests extends AbstractJettyHomeTest assertThat(run1.getExitValue(), is(0)); int httpPort1 = Tester.freePort(); - try (JettyHomeTester.Run run2 = distribution.start(List.of("jetty.http.port=" + httpPort1))) + String origin = "http://localhost:" + httpPort1; + List args = List.of( + "jetty.http.port=" + httpPort1, + "jetty.crossorigin.allowedOriginPatterns=" + origin, + "jetty.crossorigin.allowCredentials=true" + ); + try (JettyHomeTester.Run run2 = distribution.start(args)) { assertThat(run2.awaitConsoleLogsFor("Started oejs.Server", START_TIMEOUT, TimeUnit.SECONDS), is(true)); startHttpClient(); ContentResponse response = client.newRequest("http://localhost:" + httpPort1 + "/demo-handler/") - .headers(headers -> headers.put(HttpHeader.ORIGIN, "http://localhost:" + httpPort1)) + .headers(headers -> headers.put(HttpHeader.ORIGIN, origin)) .timeout(15, TimeUnit.SECONDS) .send(); @@ -1831,13 +1837,14 @@ public class DistributionTests extends AbstractJettyHomeTest assertThat(response.getContentAsString(), containsString("Hello World")); // Verify that the CORS headers are present. assertTrue(response.getHeaders().contains(HttpHeader.ACCESS_CONTROL_ALLOW_ORIGIN)); + assertTrue(response.getHeaders().contains(HttpHeader.ACCESS_CONTROL_ALLOW_CREDENTIALS)); } int httpPort2 = Tester.freePort(); - List args = List.of( + args = List.of( "jetty.http.port=" + httpPort2, - // Allow a different origin. - "jetty.crossorigin.allowedOriginPatterns=http://localhost" + // Allow only a different origin, so cross-origin requests will fail. + "jetty.crossorigin.allowedOriginPatterns=" + origin ); try (JettyHomeTester.Run run2 = distribution.start(args)) {