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
4 changes: 4 additions & 0 deletions changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@ Changes log
===========

- 2.6.2 (??-??-2026)
- Misc
- 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
- Missing throw in JsonConverter.java. Issue #729.
- Type mismatch on container access in MethodAnnotationInfo. Issue #1278.
- Boolean canAppend in ZipClientHelper can only be false. Issue #1279.
- 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.6.1 (30-09-2026)
- Bugs fixed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,18 @@
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Predicate;

/**
* 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.
*
* Example:
*
* <pre>
Expand Down Expand Up @@ -60,6 +67,9 @@ public class CorsFilter extends Filter {
/** The value of '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 @@ -183,6 +193,12 @@ 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 @@ -281,6 +297,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.6.2
*/
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.6.2
*/
public CorsFilter setAllowedOriginMatcher(Predicate<String> allowedOriginMatcher) {
this.allowedOriginMatcher = allowedOriginMatcher;
this.corsResponseHelper = null;
return this;
}

/**
* Sets the value of 'Access-Control-Allow-Origin' header.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@
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;

import java.util.HashSet;
import java.util.Set;
import java.util.function.Predicate;
import java.util.logging.Logger;

/**
Expand All @@ -26,8 +28,9 @@
* 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
*/
Expand Down Expand Up @@ -56,6 +59,12 @@
/** The value of 'Access-Control-Allow-Origin' header. Default is '*'. */
public Set<String> allowedOrigins = SetUtils.newHashSet("*");

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

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

Expand All @@ -71,8 +80,16 @@
* @param request The current request.
* @param response The response.
*/
public void addCorsResponseHeaders(Request request, Response response) {

Check failure on line 83 in org.restlet.java/org.restlet/src/main/java/org/restlet/engine/application/CorsResponseHelper.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 29 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=restlet_restlet-framework-java&issues=AaGS8jVeMRLCRryoBn1e&open=AaGS8jVeMRLCRryoBn1e&pullRequest=1612

// 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 @@ -84,9 +101,9 @@
// Header 'Allow' is not relevant in CORS request.
response.getAllowedMethods().clear();

if (!allowedOrigins.contains("*") && !allowedOrigins.contains(origin)) {
if (!isOriginAllowed(origin)) {
// Origin not allowed
LOGGER.fine("Origin " + origin + " not allowed for CORS request");

Check warning on line 106 in org.restlet.java/org.restlet/src/main/java/org/restlet/engine/application/CorsResponseHelper.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use the built-in formatting to construct this argument.

See more on https://sonarcloud.io/project/issues?id=restlet_restlet-framework-java&issues=AaGS8jVeMRLCRryoBn1d&open=AaGS8jVeMRLCRryoBn1d&pullRequest=1612
return;
}

Expand Down Expand Up @@ -313,6 +330,42 @@
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.6.2
*/
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.6.2
*/
public void setAllowedOriginMatcher(Predicate<String> allowedOriginMatcher) {
this.allowedOriginMatcher = allowedOriginMatcher;
}

/**
* Sets the value of 'Access-Control-Allow-Origin' header.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,18 @@
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Predicate;

/**
* Application service that adds support of CORS. This service lets the target
* resource specifies 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.
*
* Example:
*
* <pre>
Expand All @@ -49,6 +56,9 @@ public class CorsService extends Service {
/** The value of '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
Expand Down Expand Up @@ -99,6 +109,7 @@ public CorsService(boolean enabled) {
@Override
public Filter createInboundFilter(Context context) {
return new CorsFilter().setAllowedCredentials(allowedCredentials).setAllowedOrigins(allowedOrigins)
.setAllowedOriginMatcher(allowedOriginMatcher)
.setAllowingAllRequestedHeaders(allowingAllRequestedHeaders).setAllowedHeaders(allowedHeaders)
.setExposedHeaders(exposedHeaders).setSkippingResourceForCorsOptions(skippingResourceForCorsOptions)
.setDefaultAllowedMethods(getDefaultAllowedMethods()).setMaxAge(maxAge);
Expand Down Expand Up @@ -221,6 +232,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.6.2
*/
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.6.2
*/
public void setAllowedOriginMatcher(Predicate<String> allowedOriginMatcher) {
this.allowedOriginMatcher = allowedOriginMatcher;
}

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