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