This is an automated email from the ASF dual-hosted git repository. Croway pushed a commit to branch camel-4.22.x in repository https://gitbox.apache.org/repos/asf/camel.git
commit d2c07bbb07d4a612854f5ea369ddf09ba147b32f Author: Andrea Cosentino <[email protected]> AuthorDate: Mon Aug 31 10:08:26 2026 +0200 CAMEL-24450: camel-jetty - do not grant CORS credentials to an origin the operator did not name (#25829) enableCORS=true added new CrossOriginFilter() with no init parameters, so Jetty's own defaults applied. Confirmed against jetty-ee10-servlets 12.1.12: DEFAULT_ALLOWED_ORIGINS is "*" and credentials default to true. The filter reflects the request's origin rather than sending "*", so that pairing is the credentialed any-origin configuration the fetch specification refuses to express - reflecting the origin being the usual way around that rule. An option named "enable CORS" should not mean "every origin, with credentials". Default allowCredentials to false when CORS is enabled. The origin is still reflected, so enabling CORS keeps working for requests that carry no credentials; an operator who needs credentialed cross-origin requests sets filterInit.allowCredentials=true and names the origins in filterInit.allowedOrigins. Asking for credentials while leaving the origins at "*" is logged as a warning, since that combination reproduces the original behaviour. The defaults are applied where the init parameter map is built, not where the filter is added: the map is handed to the endpoint earlier and only when it is non-empty, so applying them later would drop them in exactly the case that matters - enableCORS on its own, with no filterInit parameters at all. EnableCORSTest.testCORSenabled asserted that credentials are granted, so it encoded the previous behaviour; it now asserts the opposite, and a second test covers the opt-in. Matches the change made to camel-platform-http-vertx under CAMEL-24436. Signed-off-by: Andrea Cosentino <[email protected]> (cherry picked from commit 6e3e4ec510b4966c70ad99b3c032e27d6adbef9d) Co-authored-by: Claude Opus 5 (1M context) <[email protected]> --- .../camel/component/jetty/JettyHttpComponent.java | 34 ++++++++++++++++++ .../camel/component/jetty/EnableCORSTest.java | 42 ++++++++++++++++++++-- 2 files changed, 74 insertions(+), 2 deletions(-) diff --git a/components/camel-jetty-common/src/main/java/org/apache/camel/component/jetty/JettyHttpComponent.java b/components/camel-jetty-common/src/main/java/org/apache/camel/component/jetty/JettyHttpComponent.java index a82c5217795e..d99f7883407d 100644 --- a/components/camel-jetty-common/src/main/java/org/apache/camel/component/jetty/JettyHttpComponent.java +++ b/components/camel-jetty-common/src/main/java/org/apache/camel/component/jetty/JettyHttpComponent.java @@ -201,6 +201,10 @@ public abstract class JettyHttpComponent extends HttpCommonComponent // extract filterInit. parameters Map filterInitParameters = PropertiesHelper.extractProperties(parameters, "filterInit."); + if (Boolean.TRUE.equals(enableCors)) { + // has to happen before the map is handed to the endpoint below, which is skipped when it is empty + applyCorsDefaults(filterInitParameters); + } URI addressUri = new URI(UnsafeUriCharactersEncoder.encodeHttpURI(remaining)); URI endpointUri = URISupport.createRemainingURI(addressUri, parameters); @@ -427,6 +431,36 @@ public abstract class JettyHttpComponent extends HttpCommonComponent } } + /** + * Supplies the CORS defaults Camel wants, for the init parameters the operator did not set. + * <p> + * {@code new CrossOriginFilter()} with no init parameters takes Jetty's own defaults, which are + * {@code allowedOrigins=*} together with {@code allowCredentials=true}. Since the filter reflects the request's + * origin rather than sending {@code *}, that is the credentialed any-origin configuration the fetch specification + * refuses to express - reflection being the usual way around that rule. An option named "enable CORS" should not + * mean "every origin, with credentials". + * <p> + * Credentials therefore default to off. Reflection of the origin is left as it was, so enabling CORS keeps working + * for requests that carry no credentials; an operator who needs credentialed cross-origin requests sets + * {@code filterInit.allowCredentials=true} and is expected to name the origins in {@code filterInit.allowedOrigins} + * at the same time, which is warned about here if they do not. + */ + @SuppressWarnings("unchecked") + private void applyCorsDefaults(Map<String, Object> filterInitParameters) { + Object configuredCredentials = filterInitParameters.get(CrossOriginFilter.ALLOW_CREDENTIALS_PARAM); + if (configuredCredentials == null) { + filterInitParameters.put(CrossOriginFilter.ALLOW_CREDENTIALS_PARAM, "false"); + return; + } + Object configuredOrigins = filterInitParameters.get(CrossOriginFilter.ALLOWED_ORIGINS_PARAM); + if (Boolean.parseBoolean(configuredCredentials.toString()) + && (configuredOrigins == null || "*".equals(configuredOrigins.toString().trim()))) { + LOG.warn("enableCORS is configured with {}=true and no specific {}." + + " Every origin will be able to make credentialed cross-origin requests to this endpoint.", + CrossOriginFilter.ALLOW_CREDENTIALS_PARAM, CrossOriginFilter.ALLOWED_ORIGINS_PARAM); + } + } + private void setFilters(JettyHttpEndpoint endpoint, Server server) { ServletContextHandler context = server.getDescendant(ServletContextHandler.class); List<Filter> filters = endpoint.getFilters(); diff --git a/components/camel-jetty/src/test/java/org/apache/camel/component/jetty/EnableCORSTest.java b/components/camel-jetty/src/test/java/org/apache/camel/component/jetty/EnableCORSTest.java index 5d067835054d..43786d86cad7 100644 --- a/components/camel-jetty/src/test/java/org/apache/camel/component/jetty/EnableCORSTest.java +++ b/components/camel-jetty/src/test/java/org/apache/camel/component/jetty/EnableCORSTest.java @@ -17,11 +17,13 @@ package org.apache.camel.component.jetty; import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.test.AvailablePortFinder; import org.apache.hc.client5.http.classic.methods.HttpGet; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; import org.apache.hc.client5.http.impl.classic.HttpClients; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; @@ -29,6 +31,13 @@ import static org.junit.jupiter.api.Assertions.assertTrue; public class EnableCORSTest extends BaseJettyTest { + @RegisterExtension + static AvailablePortFinder.Port port3 = AvailablePortFinder.find(); + + private static int getPort3() { + return port3.getPort(); + } + @Test public void testCORSdisabled() throws Exception { HttpGet httpMethod = new HttpGet("http://localhost:" + getPort() + "/test1"); @@ -44,19 +53,45 @@ public class EnableCORSTest extends BaseJettyTest { } } + /** + * enableCORS on its own reflects the request origin, which is what makes CORS work at all, but must not also grant + * credentials: reflecting the origin is the usual way around the fetch specification's refusal to pair "*" with + * credentials, so the two together are the credentialed any-origin configuration. + */ @Test - public void testCORSenabled() throws Exception { + public void testCORSenabledDoesNotGrantCredentials() throws Exception { HttpGet httpMethod = new HttpGet("http://localhost:" + getPort2() + "/test2"); httpMethod.addHeader("Origin", "http://localhost:9000"); httpMethod.addHeader("Referer", "http://localhost:9000"); + try (CloseableHttpClient client = HttpClients.createDefault(); + CloseableHttpResponse response = client.execute(httpMethod)) { + + assertEquals(200, response.getCode(), "Get a wrong response status"); + + // the origin is still reflected, so CORS itself keeps working + assertEquals("http://localhost:9000", response.getFirstHeader("Access-Control-Allow-Origin").getValue()); + + Object credentials = response.getFirstHeader("Access-Control-Allow-Credentials"); + assertTrue(credentials == null + || !Boolean.parseBoolean(response.getFirstHeader("Access-Control-Allow-Credentials").getValue()), + "credentials must not be granted to an origin the operator did not name"); + } + } + + @Test + public void testCORSCredentialsCanBeAskedFor() throws Exception { + HttpGet httpMethod = new HttpGet("http://localhost:" + getPort3() + "/test3"); + httpMethod.addHeader("Origin", "http://localhost:9000"); + httpMethod.addHeader("Referer", "http://localhost:9000"); + try (CloseableHttpClient client = HttpClients.createDefault(); CloseableHttpResponse response = client.execute(httpMethod)) { assertEquals(200, response.getCode(), "Get a wrong response status"); String responseHeader = response.getFirstHeader("Access-Control-Allow-Credentials").getValue(); - assertTrue(Boolean.parseBoolean(responseHeader), "CORS not enabled"); + assertTrue(Boolean.parseBoolean(responseHeader), "credentials should be granted when configured"); } } @@ -66,6 +101,9 @@ public class EnableCORSTest extends BaseJettyTest { public void configure() { from("jetty://http://localhost:{{port}}/test1?enableCORS=false").transform(simple("OK")); from("jetty://http://localhost:{{port2}}/test2?enableCORS=true").transform(simple("OK")); + from("jetty://http://localhost:" + getPort3() + "/test3?enableCORS=true" + + "&filterInit.allowedOrigins=http://localhost:9000" + + "&filterInit.allowCredentials=true").transform(simple("OK")); } }; }
