Skip to content
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
3 changes: 3 additions & 0 deletions changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@ Changes log
- Upgraded Jackson library to version 2.22.4.
- Upgraded org.json library to version 20260814.
- Deprecated the legacy RFC 2109/2965 cookie API (cookie version, comment and related constructors), to be removed in Restlet 3.0 in favor of RFC 6265.
- Added an optional origin matcher to CORS (`CorsService`, `CorsFilter`, `setAllowedOriginMatcher(Predicate<String>)`) to accept origins that are not listed, for example any sub-domain. The response always contains the exact request origin; it has no effect while the allowed origins contain `*`. Issue #1611.
- CorsService and CorsFilter now log a warning when `*` is allowed as origin together with credentials (any web site could then send credentialed requests), and their Javadoc references the Fetch standard instead of the W3C CORS draft. Issue #1611.
- Bugs fixed
- Slf4j bridge reports wrong line number and caller class in log4j. Issue #233.
- When GET is an allowed method of a server resource, HEAD is now also listed (e.g. in the Allow header of a 405 response). Issue #266.
- The Max-Age and Path attributes of a version 1 cookie are no longer quoted in the Set-Cookie header, as browsers ignore them otherwise. Issue #1334.
- CookieAuthenticator no longer lets the next restlet overwrite the redirection sent after a login. Issue #554.
- CorsService and CorsFilter now add `Vary: Origin` to every response when the CORS answer depends on the request origin (origin allow-list or credentials allowed), including requests without an `Origin` header, so shared caches no longer serve the response of one origin to another. A static `*` without credentials still sends no `Vary`, as recommended by the Fetch standard. Issue #1611.

- 2.7.0 (30-09-2026)
- Misc
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Predicate;
import org.restlet.Context;
import org.restlet.Request;
import org.restlet.Response;
Expand All @@ -23,6 +24,12 @@
* Filter that helps support CORS requests. This filter lets the target resources specify the
* allowed methods.
*
* <p><b>Security note:</b> the Fetch standard does not accept the wildcard {@code *} in {@code
* Access-Control-Allow-Origin} for requests with credentials, so when credentials are allowed the
* request origin is echoed back. Combining {@code *} in the allowed origins with allowed
* credentials therefore lets any web site send credentialed requests: list the allowed origins
* explicitly instead.
*
* <p>Example:
*
* <pre>
Expand Down Expand Up @@ -56,6 +63,11 @@ public class CorsFilter extends Filter {
/** The value of the 'Access-Control-Allow-Origin' header. Default is '*'. */
private Set<String> allowedOrigins = SetUtils.newHashSet("*");

/**
* Optional additional rule to accept an origin that is not listed in {@link #allowedOrigins}.
*/
private Predicate<String> allowedOriginMatcher = null;

/** Helper for generating CORS response. */
private CorsResponseHelper corsResponseHelper;

Expand Down Expand Up @@ -174,6 +186,13 @@ protected CorsResponseHelper getCorsResponseHelper() {
corsResponseHelper.setAllowedHeaders(allowedHeaders);
corsResponseHelper.setExposedHeaders(exposedHeaders);
corsResponseHelper.setMaxAge(maxAge);
corsResponseHelper.setAllowedOriginMatcher(allowedOriginMatcher);
if (allowedCredentials && allowedOrigins != null && allowedOrigins.contains("*")) {
getLogger()
.warning(
"CORS: allowed origins contain '*' and credentials are allowed, so any web site "
+ "can send credentialed requests. List the allowed origins explicitly.");
}
}
return corsResponseHelper;
}
Expand Down Expand Up @@ -269,6 +288,32 @@ public CorsFilter setAllowedHeaders(Set<String> allowedHeaders) {
return this;
}

/**
* Returns the optional rule used to accept origins that are not listed in the allowed origins.
*
* @return The origin matcher, or null.
* @since 2.7.1
*/
public Predicate<String> getAllowedOriginMatcher() {
return allowedOriginMatcher;
}

/**
* Sets an optional rule to accept origins that are not listed in the allowed origins, for
* example {@code origin -> origin.endsWith(".example.com")}. The response always contains the
* exact request origin. The set of allowed origins must not contain "*", otherwise every origin
* is accepted. By default no matcher is used.
*
* @param allowedOriginMatcher The origin matcher, or null for none.
* @return Itself for chaining methods calls.
* @since 2.7.1
*/
public CorsFilter setAllowedOriginMatcher(Predicate<String> allowedOriginMatcher) {
this.allowedOriginMatcher = allowedOriginMatcher;
this.corsResponseHelper = null;
return this;
}

/**
* Sets the value of the 'Access-Control-Allow-Origin' header.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@

import java.util.HashSet;
import java.util.Set;
import java.util.function.Predicate;
import java.util.logging.Logger;
import org.restlet.Context;
import org.restlet.Request;
import org.restlet.Response;
import org.restlet.data.Dimension;
import org.restlet.data.Method;
import org.restlet.data.Status;
import org.restlet.engine.util.SetUtils;
Expand All @@ -23,8 +25,11 @@
* The CORS specification defines a subset of methods qualified as simple HEAD, GET and POST. Any
* other methods should send a preflight request with the method OPTIONS.
*
* @see <a href="http://www.w3.org/TR/cors">W3C CORS Specification</a>
* @see <a href="http://www.w3.org/TR/cors/#simple-method">Simple methods</a>
* @see <a href="https://fetch.spec.whatwg.org/#http-cors-protocol">Fetch Standard, CORS
* protocol</a>
* @see <a href="https://fetch.spec.whatwg.org/#cors-protocol-and-http-caches">Fetch Standard, CORS
* protocol and HTTP caches</a>
* @see <a href="https://www.rfc-editor.org/rfc/rfc9110#section-12.5.5">RFC 9110, Vary</a>
* @author Manuel Boillod
*/
public class CorsResponseHelper {
Expand All @@ -50,6 +55,12 @@ public class CorsResponseHelper {
/** The value of the 'Access-Control-Allow-Origin' header. Default is '*'. */
private Set<String> allowedOrigins = SetUtils.newHashSet("*");

/**
* Optional additional rule to accept an origin that is not listed in {@link #allowedOrigins}.
* Default is null.
*/
private Predicate<String> allowedOriginMatcher = null;

/** The value of 'Access-Control-Expose-Headers' response header. */
private Set<String> exposedHeaders = null;

Expand All @@ -66,6 +77,14 @@ public class CorsResponseHelper {
*/
public void addCorsResponseHeaders(Request request, Response response) {

// When the answer depends on the Origin request header (allow-list or credentials),
// caches must key on it, including for requests without Origin, whose response lacks
// CORS headers (Fetch standard, "CORS protocol and HTTP caches"; RFC 9110, 12.5.5).
// A static "*" must not use Vary.
if (allowedCredentials || !allowedOrigins.contains("*")) {
response.getDimensions().add(Dimension.ORIGIN);
}

String origin = request.getHeaders().getFirstValue("Origin", true);

if (origin == null) {
Expand All @@ -77,7 +96,7 @@ public void addCorsResponseHeaders(Request request, Response response) {
// Header 'Allow' is not relevant in a CORS request.
response.getAllowedMethods().clear();

if (!allowedOrigins.contains("*") && !allowedOrigins.contains(origin)) {
if (!isOriginAllowed(origin)) {
// Origin isn't allowed
LOGGER.fine(() -> "Origin " + origin + " not allowed for CORS request");
return;
Expand Down Expand Up @@ -301,6 +320,42 @@ public void setAllowedHeaders(Set<String> allowedHeaders) {
this.allowedHeaders = allowedHeaders;
}

/**
* Returns the optional rule used to accept origins that are not listed in the allowed origins.
*
* @return The origin matcher, or null.
* @since 2.7.1
*/
public Predicate<String> getAllowedOriginMatcher() {
return allowedOriginMatcher;
}

/**
* Indicates if the given request origin is allowed: "*", a listed origin, or accepted by the
* origin matcher. The comparison is exact and case-sensitive, as in the Fetch standard.
*
* @param origin The value of the 'Origin' request header.
* @return True if the origin is allowed.
*/
private boolean isOriginAllowed(String origin) {
return allowedOrigins.contains("*")
|| allowedOrigins.contains(origin)
|| (allowedOriginMatcher != null && allowedOriginMatcher.test(origin));
}

/**
* Sets an optional rule to accept origins that are not listed in the allowed origins, for
* example {@code origin -> origin.endsWith(".example.com")}. The response always contains the
* exact request origin. The set of allowed origins must not contain "*", otherwise every origin
* is accepted. By default no matcher is used.
*
* @param allowedOriginMatcher The origin matcher, or null for none.
* @since 2.7.1
*/
public void setAllowedOriginMatcher(Predicate<String> allowedOriginMatcher) {
this.allowedOriginMatcher = allowedOriginMatcher;
}

/**
* Sets the value of the 'Access-Control-Allow-Origin' header.
*
Expand Down
34 changes: 34 additions & 0 deletions org.restlet/src/main/java/org/restlet/service/CorsService.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Predicate;
import org.restlet.Context;
import org.restlet.data.Method;
import org.restlet.engine.application.CorsFilter;
Expand All @@ -21,6 +22,12 @@
* Application service that adds support of CORS. This service lets the target resource specify the
* allowed methods.
*
* <p><b>Security note:</b> the Fetch standard does not accept the wildcard {@code *} in {@code
* Access-Control-Allow-Origin} for requests with credentials, so when credentials are allowed the
* request origin is echoed back. Combining {@code *} in the allowed origins with allowed
* credentials therefore lets any web site send credentialed requests: list the allowed origins
* explicitly instead.
*
* <p>Example:
*
* <pre>
Expand All @@ -45,6 +52,9 @@ public class CorsService extends Service {
/** The value of the 'Access-Control-Allow-Origin' header. Default is '*'. */
private Set<String> allowedOrigins = SetUtils.newHashSet("*");

/** Optional additional rule to accept an origin that is not listed in the allowed origins. */
private Predicate<String> allowedOriginMatcher = null;

/**
* If true, copies the value of 'Access-Control-Request-Headers' request header into the
* 'Access-Control-Allow-Headers' response header. If false, use {@link #allowedHeaders}.
Expand Down Expand Up @@ -94,6 +104,7 @@ public CorsService(boolean enabled) {
@Override
public Filter createInboundFilter(Context context) {
return new CorsFilter()
.setAllowedOriginMatcher(allowedOriginMatcher)
.setAllowedCredentials(allowedCredentials)
.setAllowedOrigins(allowedOrigins)
.setAllowingAllRequestedHeaders(allowingAllRequestedHeaders)
Expand Down Expand Up @@ -216,6 +227,29 @@ public void setAllowedHeaders(Set<String> allowedHeaders) {
this.allowedHeaders = allowedHeaders;
}

/**
* Returns the optional rule used to accept origins that are not listed in the allowed origins.
*
* @return The origin matcher, or null.
* @since 2.7.1
*/
public Predicate<String> getAllowedOriginMatcher() {
return allowedOriginMatcher;
}

/**
* Sets an optional rule to accept origins that are not listed in the allowed origins, for
* example {@code origin -> origin.endsWith(".example.com")}. The response always contains the
* exact request origin. The set of allowed origins must not contain "*", otherwise every origin
* is accepted. By default no matcher is used.
*
* @param allowedOriginMatcher The origin matcher, or null for none.
* @since 2.7.1
*/
public void setAllowedOriginMatcher(Predicate<String> allowedOriginMatcher) {
this.allowedOriginMatcher = allowedOriginMatcher;
}

/**
* Sets the value of the 'Access-Control-Allow-Origin' header.
*
Expand Down
Loading
Loading