Changed CrossOriginHandler default to allow no origin and no credentials.

This makes the default configuration more secure and explicitly requires configuration from users.

Signed-off-by: Simone Bordet <simone.bordet@gmail.com>
This commit is contained in:
Simone Bordet 2024-02-29 10:08:12 +01:00
parent 4aeec060ac
commit 561b8da4dd
8 changed files with 94 additions and 39 deletions

View File

@ -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.

View File

@ -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]]

View File

@ -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.

View File

@ -5,9 +5,7 @@
<Call name="insertHandler">
<Arg>
<New id="CrossOriginHandler" class="org.eclipse.jetty.server.handler.CrossOriginHandler">
<Set name="allowCredentials">
<Property name="jetty.crossorigin.allowCredentials" default="true" />
</Set>
<Set name="allowCredentials" property="jetty.crossorigin.allowCredentials" />
<Call name="setAllowedHeaders">
<Arg type="Set">
<Call class="org.eclipse.jetty.util.StringUtil" name="csvSplit">
@ -30,7 +28,7 @@
<Arg type="Set">
<Call class="org.eclipse.jetty.util.StringUtil" name="csvSplit">
<Arg>
<Property name="jetty.crossorigin.allowedOriginPatterns" default="*" />
<Property name="jetty.crossorigin.allowedOriginPatterns" default="" />
</Arg>
</Call>
</Arg>

View File

@ -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=

View File

@ -47,9 +47,13 @@ import org.slf4j.LoggerFactory;
* Origin: http://domain.com
* }</pre>
* <p>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}.</p>
* <p>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)}.</p>
* <p>By default, no origin is allowed, and credentials are not allowed.</p>
*/
@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<String> allowedHeaders = Set.of("Content-Type");
private Set<String> allowedMethods = Set.of("GET", "POST", "HEAD");
private Set<String> allowedOrigins = Set.of("*");
private Set<String> allowedOrigins = Set.of();
private Set<String> 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();
}

View File

@ -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.

View File

@ -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<String> 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<String> 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))
{