Skip to content

Commit

Permalink
Fixes #10220 - Implement CrossOriginHandler. (#11093)
Browse files Browse the repository at this point in the history
Introduced CrossOriginHandler.
Added cross-origin Jetty module.
Added CrossOriginHandler documentation to the programming guide.
Added CrossOriginHandler documentation to the operations guide.
Added cross-origin headers to the HttpHeader enum.
Added test cases.
Deprecated ee10 CrossOriginFilter.

Signed-off-by: Simone Bordet <simone.bordet@gmail.com>
  • Loading branch information
sbordet authored Jan 12, 2024
1 parent 581f9ae commit a9e564a
Show file tree
Hide file tree
Showing 12 changed files with 1,419 additions and 17 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//

[[og-module-cross-origin]]
===== Module `cross-origin`

The `cross-origin` module provides support for the link:https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS[CORS protocol] implemented by browsers when performing cross-origin requests.

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.

The module properties are:

----
include::{jetty-home}/modules/cross-origin.mod[tags=documentation]
----
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ include::module-alpn.adoc[]
include::module-bytebufferpool.adoc[]
include::module-console-capture.adoc[]
include::module-core-deploy.adoc[]
include::module-cross-origin.adoc[]
include::module-eeN-deploy.adoc[]
include::module-http.adoc[]
include::module-http2.adoc[]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,53 @@ Server applications must configure a `HttpConfiguration` object with the secure
include::../../{doc_code}/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=securedHandler]
----

[[pg-server-http-handler-use-cross-origin]]
====== CrossOriginHandler

`CrossOriginHandler` supports the server-side requirements of the link:https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS[CORS protocol] implemented by browsers when performing cross-origin requests.

An example of a cross-origin request is when a script downloaded from the origin domain `+http://domain.com+` uses `fetch()` or `XMLHttpRequest` to make a request to a cross domain such as `+http://cross.domain.com+` (a subdomain of the origin domain) or to `+http://example.com+` (a completely different domain).

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.

For security reasons, browsers by default do not allow cross-origin requests, unless the response from the cross domain contains the right CORS headers.

`CrossOriginHandler` relieves server-side web applications from handling CORS headers explicitly.
You can set up your `Handler` tree with the `CrossOriginHandler`, configure it, and it will take care of the CORS headers separately from your application, where you can concentrate on the business logic.

The `Handler` tree structure looks like the following:

[source,screen]
----
Server
└── CrossOriginHandler
└── ContextHandler /app
└── AppHandler
----

The most important `CrossOriginHandler` configuration parameter is `allowedOrigins`, which by default is `*`, allowing any origin.

You may want to restrict your server 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]
----
include::../../{doc_code}/org/eclipse/jetty/docs/programming/server/http/HTTPServerDocs.java[tags=crossOriginAllowedOrigins]
----

Browsers send cross-origin request 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.

Both preflight requests and cross-origin requests will be handled by `CrossOriginHandler`, which will analyze the request and possibly add appropriate CORS response headers.

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.

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]]
====== DefaultHandler

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ However, if the `WebSocketUpgradeFilter` is already present in `web.xml` under t

This allows you to customize:

* The filter order; for example, by configuring the `CrossOriginFilter` (or other filters) for increased security or authentication _before_ the `WebSocketUpgradeFilter`.
* The filter order; for example, by configuring filters for increased security or authentication _before_ the `WebSocketUpgradeFilter`.
* The `WebSocketUpgradeFilter` configuration via ``init-param``s, that affects all `Session` instances created by this filter.
* The `WebSocketUpgradeFilter` path mapping. Rather than the default mapping of `+/*+`, you can map the `WebSocketUpgradeFilter` to a more specific path such as `+/ws/*+`.
* The possibility to have multiple ``WebSocketUpgradeFilter``s, mapped to different paths, each with its own configuration.
Expand All @@ -38,14 +38,14 @@ For example:
version="5.0">
<display-name>My WebSocket WebApp</display-name>
<!-- The CrossOriginFilter *must* be the first --> <!--1-->
<!-- The SecurityFilter *must* be the first --> <!--1-->
<filter>
<filter-name>cross-origin</filter-name>
<filter-class>org.eclipse.jetty.{ee-current}.servlets.CrossOriginFilter</filter-class>
<filter-name>security</filter-name>
<filter-class>com.acme.SecurityFilter</filter-class>
<async-supported>true</async-supported>
</filter>
<filter-mapping>
<filter-name>cross-origin</filter-name>
<filter-name>security</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
Expand All @@ -69,7 +69,7 @@ For example:
</web-app>
----
<1> The `CrossOriginFilter` is the first to protect against link:https://owasp.org/www-community/attacks/csrf[cross-site request forgery attacks].
<1> The custom `SecurityFilter` is the first, to apply custom security.
<2> The configuration for the _default_ `WebSocketUpgradeFilter`.
<3> Note the use of the _default_ `WebSocketUpgradeFilter` name.
<4> Specific configuration for `WebSocketUpgradeFilter` parameters.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,20 @@
import java.nio.file.Path;
import java.security.Security;
import java.time.Duration;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;
import java.util.TimeZone;
import java.util.concurrent.CompletableFuture;

import jakarta.servlet.DispatcherType;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.conscrypt.OpenSSLProvider;
import org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory;
import org.eclipse.jetty.ee10.servlet.DefaultServlet;
import org.eclipse.jetty.ee10.servlet.FilterHolder;
import org.eclipse.jetty.ee10.servlet.ServletContextHandler;
import org.eclipse.jetty.ee10.servlet.ServletHolder;
import org.eclipse.jetty.ee10.servlets.CrossOriginFilter;
import org.eclipse.jetty.ee10.webapp.WebAppContext;
import org.eclipse.jetty.http.HttpCompliance;
import org.eclipse.jetty.http.HttpFields;
Expand Down Expand Up @@ -74,6 +71,7 @@
import org.eclipse.jetty.server.SslConnectionFactory;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.server.handler.ContextHandlerCollection;
import org.eclipse.jetty.server.handler.CrossOriginHandler;
import org.eclipse.jetty.server.handler.DefaultHandler;
import org.eclipse.jetty.server.handler.EventsHandler;
import org.eclipse.jetty.server.handler.QoSHandler;
Expand Down Expand Up @@ -1066,22 +1064,21 @@ public void servletContextHandler() throws Exception
Connector connector = new ServerConnector(server);
server.addConnector(connector);

// Add the CrossOriginHandler to protect from CSRF attacks.
CrossOriginHandler crossOriginHandler = new CrossOriginHandler();
server.setHandler(crossOriginHandler);

// Create a ServletContextHandler with contextPath.
ServletContextHandler context = new ServletContextHandler();
context.setContextPath("/shop");
// Link the context to the server.
server.setHandler(context);
crossOriginHandler.setHandler(context);

// Add the Servlet implementing the cart functionality to the context.
ServletHolder servletHolder = context.addServlet(ShopCartServlet.class, "/cart/*");
// Configure the Servlet with init-parameters.
servletHolder.setInitParameter("maxItems", "128");

// Add the CrossOriginFilter to protect from CSRF attacks.
FilterHolder filterHolder = context.addFilter(CrossOriginFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST));
// Configure the filter.
filterHolder.setAsyncSupported(true);

server.start();
// end::servletContextHandler-setup[]
}
Expand Down Expand Up @@ -1463,6 +1460,15 @@ public void securedHandler() throws Exception
// end::securedHandler[]
}

public void crossOriginAllowedOrigins()
{
// tag::crossOriginAllowedOrigins[]
CrossOriginHandler crossOriginHandler = new CrossOriginHandler();
// The allowed origins are regex patterns.
crossOriginHandler.setAllowedOriginPatterns(Set.of("http://domain\\.com"));
// end::crossOriginAllowedOrigins[]
}

public void defaultHandler() throws Exception
{
// tag::defaultHandler[]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@

public enum HttpHeader
{

/**
* General Fields.
*/
Expand Down Expand Up @@ -59,6 +58,8 @@ public enum HttpHeader
ACCEPT_CHARSET("Accept-Charset"),
ACCEPT_ENCODING("Accept-Encoding"),
ACCEPT_LANGUAGE("Accept-Language"),
ACCESS_CONTROL_REQUEST_HEADERS("Access-Control-Request-Headers"),
ACCESS_CONTROL_REQUEST_METHOD("Access-Control-Request-Method"),
AUTHORIZATION("Authorization"),
EXPECT("Expect"),
FORWARDED("Forwarded"),
Expand Down Expand Up @@ -87,6 +88,12 @@ public enum HttpHeader
* Response Fields.
*/
ACCEPT_RANGES("Accept-Ranges"),
ACCESS_CONTROL_ALLOW_ORIGIN("Access-Control-Allow-Origin"),
ACCESS_CONTROL_ALLOW_METHODS("Access-Control-Allow-Methods"),
ACCESS_CONTROL_ALLOW_HEADERS("Access-Control-Allow-Headers"),
ACCESS_CONTROL_MAX_AGE("Access-Control-Max-Age"),
ACCESS_CONTROL_ALLOW_CREDENTIALS("Access-Control-Allow-Credentials"),
ACCESS_CONTROL_EXPOSE_HEADERS("Access-Control-Expose-Headers"),
AGE("Age"),
ALT_SVC("Alt-Svc"),
ETAG("ETag"),
Expand All @@ -96,6 +103,7 @@ public enum HttpHeader
RETRY_AFTER("Retry-After"),
SERVER("Server"),
SERVLET_ENGINE("Servlet-Engine"),
TIMING_ALLOW_ORIGIN("Timing-Allow-Origin"),
VARY("Vary"),
WWW_AUTHENTICATE("WWW-Authenticate"),

Expand Down
71 changes: 71 additions & 0 deletions jetty-core/jetty-server/src/main/config/etc/jetty-cross-origin.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?xml version="1.0"?>
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "https://www.eclipse.org/jetty/configure_10_0.dtd">

<Configure id="Server" class="org.eclipse.jetty.server.Server">
<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>
<Call name="setAllowedHeaders">
<Arg type="Set">
<Call class="org.eclipse.jetty.util.StringUtil" name="csvSplit">
<Arg>
<Property name="jetty.crossorigin.allowedHeaders" default="Content-Type" />
</Arg>
</Call>
</Arg>
</Call>
<Call name="setAllowedMethods">
<Arg type="Set">
<Call class="org.eclipse.jetty.util.StringUtil" name="csvSplit">
<Arg>
<Property name="jetty.crossorigin.allowedMethods" default="GET,POST,HEAD" />
</Arg>
</Call>
</Arg>
</Call>
<Call name="setAllowedOriginPatterns">
<Arg type="Set">
<Call class="org.eclipse.jetty.util.StringUtil" name="csvSplit">
<Arg>
<Property name="jetty.crossorigin.allowedOriginPatterns" default="*" />
</Arg>
</Call>
</Arg>
</Call>
<Call name="setAllowedTimingOriginPatterns">
<Arg type="Set">
<Call class="org.eclipse.jetty.util.StringUtil" name="csvSplit">
<Arg>
<Property name="jetty.crossorigin.allowedTimingOriginPatterns" default="" />
</Arg>
</Call>
</Arg>
</Call>
<Set name="deliverPreflightRequests" property="jetty.crossorigin.deliverPreflightRequests" />
<Set name="deliverNonAllowedOriginRequests" property="jetty.crossorigin.deliverNonAllowedOriginRequests" />
<Set name="deliverNonAllowedOriginWebSocketUpgradeRequests" property="jetty.crossorigin.deliverNonAllowedOriginWebSocketUpgradeRequests" />
<Call name="setExposedHeaders">
<Arg type="Set">
<Call class="org.eclipse.jetty.util.StringUtil" name="csvSplit">
<Arg>
<Property name="jetty.crossorigin.exposedHeaders" default="" />
</Arg>
</Call>
</Arg>
</Call>
<Call name="setPreflightMaxAge">
<Arg>
<Call class="java.time.Duration" name="ofSeconds">
<Arg type="long">
<Property name="jetty.crossorigin.preflightMaxAge" default="60" />
</Arg>
</Call>
</Arg>
</Call>
</New>
</Arg>
</Call>
</Configure>
48 changes: 48 additions & 0 deletions jetty-core/jetty-server/src/main/config/modules/cross-origin.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# DO NOT EDIT THIS FILE - See: https://eclipse.dev/jetty/documentation/

[description]
Enables CrossOriginHandler to support the CORS protocol and protect from cross-site request forgery (CSRF) attacks.

[tags]
server
handler
csrf

[depend]
server

[xml]
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

## A comma-separated list of headers allowed in cross-origin requests.
# jetty.crossorigin.allowedHeaders=Content-Type

## A comma-separated list of HTTP methods allowed in cross-origin requests.
# jetty.crossorigin.allowedMethods=GET,POST,HEAD

## A comma-separated list of origins regex patterns allowed in cross-origin requests.
# jetty.crossorigin.allowedOriginPatterns=*

## A comma-separated list of timing origins regex patterns allowed in cross-origin requests.
# jetty.crossorigin.allowedTimingOriginPatterns=

## Whether preflight requests are delivered to the child Handler of CrossOriginHandler.
# jetty.crossorigin.deliverPreflightRequests=false

## Whether requests whose origin is not allowed are delivered to the child Handler of CrossOriginHandler.
# jetty.crossorigin.deliverNonAllowedOriginRequests=true

## Whether WebSocket upgrade requests whose origin is not allowed are delivered to the child Handler of CrossOriginHandler.
# jetty.crossorigin.deliverNonAllowedOriginWebSocketUpgradeRequests=false

## A comma-separated list of headers allowed in cross-origin responses.
# jetty.crossorigin.exposedHeaders=

## How long the preflight results can be cached by browsers, in seconds.
# jetty.crossorigin.preflightMaxAge=60
#end::documentation[]
Loading

0 comments on commit a9e564a

Please sign in to comment.