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 b0399b181d26cd8b136fa98b54ec8e5c283338da Author: Andrea Cosentino <[email protected]> AuthorDate: Mon Aug 31 10:08:26 2026 +0200 CAMEL-24436: camel-platform-http-vertx - only allow CORS credentials for a configured origin (#25820) createCorsHandler() set Access-Control-Allow-Credentials: true outside the origin check, so it went out on every response to a request carrying an Origin header - including responses to origins the handler had just decided not to allow. And when camel.server.cors.origins is unset, allowsOrigin is true for every origin and the caller's own Origin is echoed back as Access-Control-Allow-Origin. Together those produce the credentialed any-origin configuration the fetch specification refuses to express as "*", which is why reflecting the origin is the usual way around that rule. Send Access-Control-Allow-Credentials only when the request origin matched an origin the operator actually configured. With no origin list the origin is still reflected, as before, but credentials are not granted. Also add Vary: Origin whenever the origin is reflected, so a shared cache cannot serve one origin's response to another. Signed-off-by: Andrea Cosentino <[email protected]> (cherry picked from commit 4b557e4e82048515b0141f4e3594fbecf7dc741c) Co-authored-by: Claude Opus 5 (1M context) <[email protected]> --- .../http/vertx/VertxPlatformHttpServerSupport.java | 15 +++-- .../http/vertx/VertxPlatformHttpEngineTest.java | 78 ++++++++++++++++++++++ 2 files changed, 89 insertions(+), 4 deletions(-) diff --git a/components/camel-platform-http-vertx/src/main/java/org/apache/camel/component/platform/http/vertx/VertxPlatformHttpServerSupport.java b/components/camel-platform-http-vertx/src/main/java/org/apache/camel/component/platform/http/vertx/VertxPlatformHttpServerSupport.java index aaca7dc7e62a..ab5224550110 100644 --- a/components/camel-platform-http-vertx/src/main/java/org/apache/camel/component/platform/http/vertx/VertxPlatformHttpServerSupport.java +++ b/components/camel-platform-http-vertx/src/main/java/org/apache/camel/component/platform/http/vertx/VertxPlatformHttpServerSupport.java @@ -114,14 +114,21 @@ public final class VertxPlatformHttpServerSupport { corsConfig.getHeaders()); } - final boolean allowsOrigin - = ObjectHelper.isEmpty(corsConfig.getOrigins()) || corsConfig.getOrigins().contains(origin); + // With no origin list configured the request origin is simply reflected back. That is the + // standard way around the fetch spec's rule that "*" and credentials are mutually exclusive, + // so credentials are only allowed when the operator actually named the origins. + final boolean explicitOrigins = ObjectHelper.isNotEmpty(corsConfig.getOrigins()); + final boolean allowsOrigin = !explicitOrigins || corsConfig.getOrigins().contains(origin); if (allowsOrigin) { response.headers().set(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, origin); + // The response body varies with the request origin, so it must not be cached against + // one origin and served to another. + response.headers().add(HttpHeaders.VARY, HttpHeaders.ORIGIN); + if (explicitOrigins) { + response.headers().set(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); + } } - response.headers().set(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); - if (ObjectHelper.isNotEmpty(corsConfig.getExposedHeaders())) { response.headers().set(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, String.join(",", corsConfig.getExposedHeaders())); diff --git a/components/camel-platform-http-vertx/src/test/java/org/apache/camel/component/platform/http/vertx/VertxPlatformHttpEngineTest.java b/components/camel-platform-http-vertx/src/test/java/org/apache/camel/component/platform/http/vertx/VertxPlatformHttpEngineTest.java index 2bfafb0db0fa..d81479098864 100644 --- a/components/camel-platform-http-vertx/src/test/java/org/apache/camel/component/platform/http/vertx/VertxPlatformHttpEngineTest.java +++ b/components/camel-platform-http-vertx/src/test/java/org/apache/camel/component/platform/http/vertx/VertxPlatformHttpEngineTest.java @@ -73,8 +73,10 @@ import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.emptyOrNullString; import static org.hamcrest.Matchers.emptyString; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.equalToIgnoringCase; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.Matchers.startsWith; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -425,6 +427,82 @@ public class VertxPlatformHttpEngineTest { } } + @Test + public void testEngineCORSNoOriginListDoesNotAllowCredentials() throws Exception { + // With no origins configured the request origin is reflected back. Reflection plus + // Access-Control-Allow-Credentials is exactly what the fetch spec forbids expressing as "*", + // so credentials must not be granted to an origin the operator never named. + final CamelContext context = createCamelContextForTest(configuration -> { + configuration.getCors().setEnabled(true); + configuration.getCors().setMethods(Arrays.asList("GET", "POST")); + }); + + try { + context.addRoutes(new RouteBuilder() { + @Override + public void configure() { + from("platform-http:/").transform().constant("cors"); + } + }); + context.start(); + + final String origin = "http://attacker.example"; + + given() + .header("Origin", origin) + .when() + .get("/") + .then() + .statusCode(200) + .header("Access-Control-Allow-Origin", origin) + .header("Vary", equalToIgnoringCase("origin")) + .header("Access-Control-Allow-Credentials", nullValue()); + } finally { + context.stop(); + } + } + + @Test + public void testEngineCORSAllowsCredentialsOnlyForAConfiguredOrigin() throws Exception { + final String allowed = "https://app.example"; + final CamelContext context = createCamelContextForTest(configuration -> { + configuration.getCors().setEnabled(true); + configuration.getCors().setOrigins(Arrays.asList(allowed)); + configuration.getCors().setMethods(Arrays.asList("GET", "POST")); + }); + + try { + context.addRoutes(new RouteBuilder() { + @Override + public void configure() { + from("platform-http:/").transform().constant("cors"); + } + }); + context.start(); + + given() + .header("Origin", allowed) + .when() + .get("/") + .then() + .statusCode(200) + .header("Access-Control-Allow-Origin", allowed) + .header("Access-Control-Allow-Credentials", "true"); + + // An origin outside the configured list gets neither header + given() + .header("Origin", "https://other.example") + .when() + .get("/") + .then() + .statusCode(200) + .header("Access-Control-Allow-Origin", nullValue()) + .header("Access-Control-Allow-Credentials", nullValue()); + } finally { + context.stop(); + } + } + @Test public void testMatchOnUriPrefix() throws Exception { final CamelContext context = createCamelContextForTest();
