Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes #10220 - Implement CrossOriginHandler. #11093

Merged
merged 8 commits into from
Jan 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -72,6 +69,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 @@ -1004,22 +1002,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 @@ -1401,6 +1398,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
Loading