This is an automated email from the ASF dual-hosted git repository.

Croway pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new 4b557e4e8204 CAMEL-24436: camel-platform-http-vertx - only allow CORS 
credentials for a configured origin (#25820)
4b557e4e8204 is described below

commit 4b557e4e82048515b0141f4e3594fbecf7dc741c
Author: Andrea Cosentino <[email protected]>
AuthorDate: Fri Aug 28 12:08:57 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]>
    Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
    Co-authored-by: Federico Mariani <[email protected]>
---
 .../http/vertx/VertxPlatformHttpServerSupport.java | 15 +++--
 .../http/vertx/VertxPlatformHttpEngineTest.java    | 78 ++++++++++++++++++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc    | 19 ++++++
 3 files changed, 108 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();
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
index 029f23952ec0..2bb1c4713a1c 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
@@ -703,6 +703,25 @@ inherit values set on `defaultInstance`, and 
`defaultInstance` itself remains un
 Routes that compared unmarshalled bodies by identity, or that mutated one body 
expecting the change to
 be visible on another, must be updated.
 
+=== camel-platform-http-vertx
+
+The CORS handler used to send `Access-Control-Allow-Credentials: true` on 
every response to a request
+carrying an `Origin` header — including responses to origins it had just 
decided not to allow, because
+the header was set outside the origin check. Combined with an unset 
`camel.server.cors.origins`, which
+makes the handler echo back whatever origin the caller sent, that produced the 
credentialed
+any-origin configuration the fetch specification forbids expressing as `*`.
+
+Two changes:
+
+* `Access-Control-Allow-Credentials` is now sent only when the request origin 
matched an origin the
+operator explicitly configured. With `camel.server.cors.origins` unset, the 
origin is still reflected
+back as before, but credentials are not granted.
+* A `Vary: Origin` response header is now added whenever the origin is 
reflected, so a shared cache
+cannot serve one origin's response to another.
+
+Deployments that relied on credentialed cross-origin requests must list the 
permitted origins in
+`camel.server.cors.origins`.
+
 === camel-tika
 
 The `tika:parse` producer copies the metadata of the parsed document onto the 
Camel message. Those

Reply via email to