This is an automated email from the ASF dual-hosted git repository.
jamesnetherton pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel-quarkus.git
The following commit(s) were added to refs/heads/main by this push:
new eb74559182 Document origin restrictions for vertx-websocket consumers
eb74559182 is described below
commit eb7455918294574c52e1e10de965a0f75e500819
Author: James Netherton <[email protected]>
AuthorDate: Fri Sep 4 07:33:16 2026 +0100
Document origin restrictions for vertx-websocket consumers
Consumers are mounted on the shared Quarkus HTTP router, so they sit behind
whatever authentication the application configures. Upstream only installs a
CorsHandler when allowedOriginPattern is set, so document how to restrict
the
origins that WebSocket upgrade requests are accepted from, either via the
Quarkus CORS filter or the allowedOriginPattern endpoint option.
Add tests covering both, including that the Quarkus CORS filter runs ahead
of
the Camel WebSocket route.
Fixes #9106
Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
---
.../reference/extensions/vertx-websocket.adoc | 38 ++++++++++
.../runtime/src/main/doc/usage.adoc | 37 ++++++++++
.../vertx/websocket/it/VertxWebsocketRoutes.java | 4 ++
.../vertx/websocket/it/VertxWebsocketCorsIT.java | 24 +++++++
.../vertx/websocket/it/VertxWebsocketCorsTest.java | 59 ++++++++++++++++
.../vertx/websocket/it/VertxWebsocketTest.java | 14 ++++
.../vertx/websocket/it/WebSocketHandshakes.java | 81 ++++++++++++++++++++++
7 files changed, 257 insertions(+)
diff --git a/docs/modules/ROOT/pages/reference/extensions/vertx-websocket.adoc
b/docs/modules/ROOT/pages/reference/extensions/vertx-websocket.adoc
index 5dd1e1482f..5c065c47f7 100644
--- a/docs/modules/ROOT/pages/reference/extensions/vertx-websocket.adoc
+++ b/docs/modules/ROOT/pages/reference/extensions/vertx-websocket.adoc
@@ -67,6 +67,44 @@ NOTE: While you do not need to explicitly configure the
host/port on the vertx-w
the host & port must exactly match the value of the Quarkus HTTP server
configuration values for `quarkus.http.host` and `quarkus.http.port`.
Otherwise, an exception will be thrown at runtime.
+[id="extensions-vertx-websocket-usage-restricting-websocket-origins"]
+==== Restricting WebSocket origins
+
+Since consumers are hosted on the Quarkus HTTP server, they sit behind
whatever authentication is configured for
+that server, for example via `quarkus.http.auth.permission`. WebSocket upgrade
requests are not subject to the
+same-origin policy and are exempt from CORS preflight, so a browser attaches
ambient credentials such as session
+cookies to an upgrade request issued by any page. If a consumer path is
protected by cookie based authentication
+and any origin is accepted, a page loaded in an authenticated user's browser
could open the WebSocket with that
+user's session.
+
+The Quarkus session cookie defaults limit this. The form authentication cookie
defaults to `SameSite=Strict` and
+the OIDC session cookie defaults to `SameSite=Lax`, and neither is sent on a
WebSocket handshake initiated from a
+cross-site page. If you relax those defaults (for example
`quarkus.http.auth.form.cookie-same-site=none`), or you
+authenticate with a cookie mechanism of your own, then restrict the origins
that consumers accept.
+
+The simplest option is to enable the Quarkus CORS filter, which runs ahead of
the Camel WebSocket route and rejects
+upgrade requests whose `Origin` header is neither same-origin nor listed in
`quarkus.http.cors.origins`, with a
+`403` response.
+
+[source,properties]
+----
+quarkus.http.cors.enabled = true
+----
+
+Requests carrying no `Origin` header, such as those from non-browser WebSocket
clients, are not affected.
+
+WARNING: Setting `quarkus.http.cors.origins` to `*` allows WebSocket upgrade
requests from any origin.
+
+Alternatively, restrict origins for an individual consumer with the
`allowedOriginPattern` option.
+
+[source,java]
+----
+from("vertx-websocket:/my-websocket-path?allowedOriginPattern=https://app\\.example\\.org")
+ .setBody().constant("Hello World");
+----
+
+See the xref:user-guide/security-model.adoc[Security model] for more about the
Camel Quarkus security model.
+
[id="extensions-vertx-websocket-usage-vert-x-websocket-producers"]
=== Vert.x WebSocket producers
diff --git a/extensions/vertx-websocket/runtime/src/main/doc/usage.adoc
b/extensions/vertx-websocket/runtime/src/main/doc/usage.adoc
index 2e1f4ec7eb..5459832f20 100644
--- a/extensions/vertx-websocket/runtime/src/main/doc/usage.adoc
+++ b/extensions/vertx-websocket/runtime/src/main/doc/usage.adoc
@@ -15,6 +15,43 @@ NOTE: While you do not need to explicitly configure the
host/port on the vertx-w
the host & port must exactly match the value of the Quarkus HTTP server
configuration values for `quarkus.http.host` and `quarkus.http.port`.
Otherwise, an exception will be thrown at runtime.
+==== Restricting WebSocket origins
+
+Since consumers are hosted on the Quarkus HTTP server, they sit behind
whatever authentication is configured for
+that server, for example via `quarkus.http.auth.permission`. WebSocket upgrade
requests are not subject to the
+same-origin policy and are exempt from CORS preflight, so a browser attaches
ambient credentials such as session
+cookies to an upgrade request issued by any page. If a consumer path is
protected by cookie based authentication
+and any origin is accepted, a page loaded in an authenticated user's browser
could open the WebSocket with that
+user's session.
+
+The Quarkus session cookie defaults limit this. The form authentication cookie
defaults to `SameSite=Strict` and
+the OIDC session cookie defaults to `SameSite=Lax`, and neither is sent on a
WebSocket handshake initiated from a
+cross-site page. If you relax those defaults (for example
`quarkus.http.auth.form.cookie-same-site=none`), or you
+authenticate with a cookie mechanism of your own, then restrict the origins
that consumers accept.
+
+The simplest option is to enable the Quarkus CORS filter, which runs ahead of
the Camel WebSocket route and rejects
+upgrade requests whose `Origin` header is neither same-origin nor listed in
`quarkus.http.cors.origins`, with a
+`403` response.
+
+[source,properties]
+----
+quarkus.http.cors.enabled = true
+----
+
+Requests carrying no `Origin` header, such as those from non-browser WebSocket
clients, are not affected.
+
+WARNING: Setting `quarkus.http.cors.origins` to `*` allows WebSocket upgrade
requests from any origin.
+
+Alternatively, restrict origins for an individual consumer with the
`allowedOriginPattern` option.
+
+[source,java]
+----
+from("vertx-websocket:/my-websocket-path?allowedOriginPattern=https://app\\.example\\.org")
+ .setBody().constant("Hello World");
+----
+
+See the xref:user-guide/security-model.adoc[Security model] for more about the
Camel Quarkus security model.
+
=== Vert.x WebSocket producers
Similar to above, if you want to produce messages to the internal Vert.x
WebSocket consumer, then you can omit the host and port from the endpoint URI.
diff --git
a/integration-tests/vertx-websocket/src/main/java/org/apache/camel/quarkus/component/vertx/websocket/it/VertxWebsocketRoutes.java
b/integration-tests/vertx-websocket/src/main/java/org/apache/camel/quarkus/component/vertx/websocket/it/VertxWebsocketRoutes.java
index 8cc174014e..ae1c74b446 100644
---
a/integration-tests/vertx-websocket/src/main/java/org/apache/camel/quarkus/component/vertx/websocket/it/VertxWebsocketRoutes.java
+++
b/integration-tests/vertx-websocket/src/main/java/org/apache/camel/quarkus/component/vertx/websocket/it/VertxWebsocketRoutes.java
@@ -54,6 +54,10 @@ public class VertxWebsocketRoutes extends RouteBuilder {
.setBody().simple("${header.paramA} ${header.paramB}")
.to("seda:queryParamsResult");
+
from("vertx-websocket:/origin/restricted?allowedOriginPattern=https://allowed\\.example\\.com")
+ .setBody().simple("Hello ${body}")
+ .to("vertx-websocket:/origin/restricted");
+
from("vertx-websocket:/events?fireWebSocketConnectionEvents=true")
.setBody().header(VertxWebsocketConstants.EVENT)
.to("seda:eventsResult");
diff --git
a/integration-tests/vertx-websocket/src/test/java/org/apache/camel/quarkus/component/vertx/websocket/it/VertxWebsocketCorsIT.java
b/integration-tests/vertx-websocket/src/test/java/org/apache/camel/quarkus/component/vertx/websocket/it/VertxWebsocketCorsIT.java
new file mode 100644
index 0000000000..550e08fe69
--- /dev/null
+++
b/integration-tests/vertx-websocket/src/test/java/org/apache/camel/quarkus/component/vertx/websocket/it/VertxWebsocketCorsIT.java
@@ -0,0 +1,24 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.quarkus.component.vertx.websocket.it;
+
+import io.quarkus.test.junit.QuarkusIntegrationTest;
+
+@QuarkusIntegrationTest
+class VertxWebsocketCorsIT extends VertxWebsocketCorsTest {
+
+}
diff --git
a/integration-tests/vertx-websocket/src/test/java/org/apache/camel/quarkus/component/vertx/websocket/it/VertxWebsocketCorsTest.java
b/integration-tests/vertx-websocket/src/test/java/org/apache/camel/quarkus/component/vertx/websocket/it/VertxWebsocketCorsTest.java
new file mode 100644
index 0000000000..52f981534d
--- /dev/null
+++
b/integration-tests/vertx-websocket/src/test/java/org/apache/camel/quarkus/component/vertx/websocket/it/VertxWebsocketCorsTest.java
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.quarkus.component.vertx.websocket.it;
+
+import java.net.URI;
+import java.util.Map;
+
+import io.quarkus.test.common.http.TestHTTPResource;
+import io.quarkus.test.junit.QuarkusTest;
+import io.quarkus.test.junit.QuarkusTestProfile;
+import io.quarkus.test.junit.TestProfile;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+@TestProfile(VertxWebsocketCorsTest.VertxWebsocketCorsTestProfile.class)
+@QuarkusTest
+class VertxWebsocketCorsTest {
+
+ @TestHTTPResource("/echo")
+ URI echo;
+
+ @Test
+ void crossOriginUpgradeIsRejected() {
+ assertEquals(403, WebSocketHandshakes.upgradeStatus(echo,
"https://attacker.example.com"));
+ }
+
+ @Test
+ void sameOriginUpgradeIsAccepted() {
+ assertEquals(WebSocketHandshakes.SWITCHING_PROTOCOLS,
+ WebSocketHandshakes.upgradeStatus(echo,
WebSocketHandshakes.originOf(echo)));
+ }
+
+ @Test
+ void upgradeWithoutOriginIsAccepted() {
+ assertEquals(WebSocketHandshakes.SWITCHING_PROTOCOLS,
WebSocketHandshakes.upgradeStatus(echo, null));
+ }
+
+ public static class VertxWebsocketCorsTestProfile implements
QuarkusTestProfile {
+ @Override
+ public Map<String, String> getConfigOverrides() {
+ return Map.of("quarkus.http.cors.enabled", "true");
+ }
+ }
+}
diff --git
a/integration-tests/vertx-websocket/src/test/java/org/apache/camel/quarkus/component/vertx/websocket/it/VertxWebsocketTest.java
b/integration-tests/vertx-websocket/src/test/java/org/apache/camel/quarkus/component/vertx/websocket/it/VertxWebsocketTest.java
index da3c7d6562..ffa32dc04e 100644
---
a/integration-tests/vertx-websocket/src/test/java/org/apache/camel/quarkus/component/vertx/websocket/it/VertxWebsocketTest.java
+++
b/integration-tests/vertx-websocket/src/test/java/org/apache/camel/quarkus/component/vertx/websocket/it/VertxWebsocketTest.java
@@ -65,6 +65,9 @@ class VertxWebsocketTest {
@TestHTTPResource("/events")
URI events;
+ @TestHTTPResource("/origin/restricted")
+ URI originRestricted;
+
@Test
public void testEchoWithShortFormUri() throws Exception {
String message = "From Short From URI";
@@ -325,6 +328,17 @@ class VertxWebsocketTest {
}
}
+ @Test
+ void allowedOriginPatternAcceptsMatchingOrigin() {
+ assertEquals(WebSocketHandshakes.SWITCHING_PROTOCOLS,
+ WebSocketHandshakes.upgradeStatus(originRestricted,
"https://allowed.example.com"));
+ }
+
+ @Test
+ void allowedOriginPatternRejectsNonMatchingOrigin() {
+ assertEquals(403, WebSocketHandshakes.upgradeStatus(originRestricted,
"https://attacker.example.com"));
+ }
+
static String[] getHosts() {
return new String[] {
"localhost",
diff --git
a/integration-tests/vertx-websocket/src/test/java/org/apache/camel/quarkus/component/vertx/websocket/it/WebSocketHandshakes.java
b/integration-tests/vertx-websocket/src/test/java/org/apache/camel/quarkus/component/vertx/websocket/it/WebSocketHandshakes.java
new file mode 100644
index 0000000000..93ad8e61e4
--- /dev/null
+++
b/integration-tests/vertx-websocket/src/test/java/org/apache/camel/quarkus/component/vertx/websocket/it/WebSocketHandshakes.java
@@ -0,0 +1,81 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.quarkus.component.vertx.websocket.it;
+
+import java.net.URI;
+import java.util.concurrent.CompletionException;
+
+import io.vertx.core.Vertx;
+import io.vertx.core.http.HttpHeaders;
+import io.vertx.core.http.UpgradeRejectedException;
+import io.vertx.core.http.WebSocket;
+import io.vertx.core.http.WebSocketClient;
+import io.vertx.core.http.WebSocketConnectOptions;
+
+final class WebSocketHandshakes {
+
+ static final int SWITCHING_PROTOCOLS = 101;
+
+ private WebSocketHandshakes() {
+ }
+
+ /**
+ * The origin that the Quarkus HTTP server is serving the given URI from.
+ */
+ static String originOf(URI uri) {
+ return "http://" + uri.getHost() + ":" + uri.getPort();
+ }
+
+ /**
+ * Attempts a WebSocket upgrade with the given {@code Origin} request
header, or with no {@code Origin} header at
+ * all when {@code origin} is {@code null}.
+ *
+ * @return {@value #SWITCHING_PROTOCOLS} if the upgrade succeeded, else
the HTTP status the handshake was rejected
+ * with
+ */
+ static int upgradeStatus(URI uri, String origin) {
+ Vertx vertx = Vertx.vertx();
+ try {
+ WebSocketClient client = vertx.createWebSocketClient();
+ WebSocketConnectOptions options = new WebSocketConnectOptions()
+ .setHost(uri.getHost())
+ .setPort(uri.getPort())
+ .setURI(uri.getPath())
+ // Vert.x strips any Origin header when allowOriginHeader
is false, so it can only be disabled
+ // when the handshake is meant to carry no Origin at all.
Otherwise leave it enabled and set the
+ // header explicitly, which Vert.x and Netty both leave
untouched.
+ .setAllowOriginHeader(origin != null);
+
+ if (origin != null) {
+ options.putHeader(HttpHeaders.ORIGIN, origin);
+ }
+
+ try {
+ WebSocket webSocket =
client.connect(options).toCompletionStage().toCompletableFuture().join();
+
webSocket.close().toCompletionStage().toCompletableFuture().join();
+ return SWITCHING_PROTOCOLS;
+ } catch (CompletionException e) {
+ if (e.getCause() instanceof UpgradeRejectedException rejected)
{
+ return rejected.getStatus();
+ }
+ throw e;
+ }
+ } finally {
+ vertx.close().toCompletionStage().toCompletableFuture().join();
+ }
+ }
+}