This is an automated email from the ASF dual-hosted git repository.
oscerd 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 9c18465443a0 CAMEL-24412: camel-netty-http - evaluate the security
constraint against the same normalized target as dispatch (#25578)
9c18465443a0 is described below
commit 9c18465443a0df60179e6abf96f7794983cd0637
Author: Andrea Cosentino <[email protected]>
AuthorDate: Sat Aug 22 21:09:49 2026 +0200
CAMEL-24412: camel-netty-http - evaluate the security constraint against
the same normalized target as dispatch (#25578)
HttpServerChannelHandler.extractTarget() strips the endpoint context-path
from
the request target so the security constraint is evaluated relative to the
endpoint. The strip was guarded by a case-sensitive startsWith:
if (path != null && target.startsWith(path)) {
// need to match by lower case as we want to ignore case on
context-path
path = path.toLowerCase(Locale.US);
String match = target.toLowerCase(Locale.US);
if (match.startsWith(path)) {
so the inner case-insensitive comparison could never change the outcome -
it was
dead code. A request whose context-path differed only by case was evaluated
against the unstripped target.
Dispatch does not share that property:
RestConsumerContextPathMatcher.matchPath()
compares with equalsIgnoreCase and a lower-cased prefix, so the request
still
reaches the route. Authorization and dispatch therefore disagreed about
which
endpoint a request belongs to. With matchOnUriPrefix=true and a
securityConstraint
whose inclusions are specific sub-paths rather than a catch-all, the
miscased
target matched no inclusion, and an unmatched target counts as unrestricted.
The strip now uses the case-insensitive comparison directly.
The added test covers both directions against a constraint with a specific
/admin/* inclusion: the exact-case path is challenged as before, and the
differently-cased path - which reaches the route either way - is now
challenged
too. Without this fix the second case returns 200 with no challenge.
Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
---
.../http/handlers/HttpServerChannelHandler.java | 14 ++--
...HttpBasicAuthConstraintCaseInsensitiveTest.java | 88 ++++++++++++++++++++++
.../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc | 16 ++++
3 files changed, 110 insertions(+), 8 deletions(-)
diff --git
a/components/camel-netty-http/src/main/java/org/apache/camel/component/netty/http/handlers/HttpServerChannelHandler.java
b/components/camel-netty-http/src/main/java/org/apache/camel/component/netty/http/handlers/HttpServerChannelHandler.java
index fedb184b99e4..790ff8cc4903 100644
---
a/components/camel-netty-http/src/main/java/org/apache/camel/component/netty/http/handlers/HttpServerChannelHandler.java
+++
b/components/camel-netty-http/src/main/java/org/apache/camel/component/netty/http/handlers/HttpServerChannelHandler.java
@@ -217,15 +217,13 @@ public class HttpServerChannelHandler extends
ServerChannelHandler {
private String extractTarget(URI uri) {
String target = uri.getPath();
- // strip the starting endpoint path so the target is relative to the
endpoint uri
+ // strip the starting endpoint path so the target is relative to the
endpoint uri.
+ // the comparison must ignore case on the context-path, the same way
consumer dispatch does
+ // (RestConsumerContextPathMatcher), so the security constraint is
evaluated against the same
+ // normalized target the request is actually routed to
String path = consumer.getConfiguration().getPath();
- if (path != null && target.startsWith(path)) {
- // need to match by lower case as we want to ignore case on
context-path
- path = path.toLowerCase(Locale.US);
- String match = target.toLowerCase(Locale.US);
- if (match.startsWith(path)) {
- target = target.substring(path.length());
- }
+ if (path != null &&
target.toLowerCase(Locale.US).startsWith(path.toLowerCase(Locale.US))) {
+ target = target.substring(path.length());
}
return target;
}
diff --git
a/components/camel-netty-http/src/test/java/org/apache/camel/component/netty/http/NettyHttpBasicAuthConstraintCaseInsensitiveTest.java
b/components/camel-netty-http/src/test/java/org/apache/camel/component/netty/http/NettyHttpBasicAuthConstraintCaseInsensitiveTest.java
new file mode 100644
index 000000000000..6dd72458a919
--- /dev/null
+++
b/components/camel-netty-http/src/test/java/org/apache/camel/component/netty/http/NettyHttpBasicAuthConstraintCaseInsensitiveTest.java
@@ -0,0 +1,88 @@
+/*
+ * 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.component.netty.http;
+
+import org.apache.camel.BindToRegistry;
+import org.apache.camel.CamelExecutionException;
+import org.apache.camel.builder.RouteBuilder;
+import org.junit.jupiter.api.Test;
+
+import static org.apache.camel.test.junit6.TestSupport.assertIsInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/**
+ * Consumer dispatch matches the context-path case-insensitively, so the
security constraint has to be evaluated against
+ * the same normalized target. Otherwise a request that differs from the
configured context-path only by case reaches
+ * the route while skipping the constraint that guards it.
+ */
+public class NettyHttpBasicAuthConstraintCaseInsensitiveTest extends
BaseNettyTestSupport {
+
+ @Override
+ public void doPreSetup() {
+ System.setProperty("java.security.auth.login.config",
"src/test/resources/myjaas.config");
+ }
+
+ @Override
+ public void doPostTearDown() {
+ System.clearProperty("java.security.auth.login.config");
+ }
+
+ @BindToRegistry("mySecurityConfig")
+ public NettyHttpSecurityConfiguration loadSecConf() {
+ NettyHttpSecurityConfiguration security = new
NettyHttpSecurityConfiguration();
+ security.setRealm("karaf");
+ SecurityAuthenticator auth = new JAASSecurityAuthenticator();
+ auth.setName("karaf");
+ security.setSecurityAuthenticator(auth);
+
+ // a specific inclusion, not a catch-all: only /admin/* below the
endpoint path is restricted
+ SecurityConstraintMapping matcher = new SecurityConstraintMapping();
+ matcher.addInclusion("/admin/*");
+ security.setSecurityConstraint(matcher);
+
+ return security;
+ }
+
+ @Test
+ public void exactCaseContextPathIsChallenged() {
+ CamelExecutionException e = assertThrows(CamelExecutionException.class,
+ () ->
template.requestBody("netty-http:http://localhost:{{port}}/foo/admin/x",
"Hello", String.class));
+ NettyHttpOperationFailedException cause =
assertIsInstanceOf(NettyHttpOperationFailedException.class, e.getCause());
+ assertEquals(401, cause.getStatusCode());
+ }
+
+ @Test
+ public void differentlyCasedContextPathIsChallengedToo() {
+ // dispatch reaches the route either way, so the constraint must apply
either way
+ CamelExecutionException e = assertThrows(CamelExecutionException.class,
+ () ->
template.requestBody("netty-http:http://localhost:{{port}}/Foo/admin/x",
"Hello", String.class));
+ NettyHttpOperationFailedException cause =
assertIsInstanceOf(NettyHttpOperationFailedException.class, e.getCause());
+ assertEquals(401, cause.getStatusCode());
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+
from("netty-http:http://0.0.0.0:{{port}}/foo?matchOnUriPrefix=true&securityConfiguration=#mySecurityConfig")
+ .transform().constant("Bye World");
+ }
+ };
+ }
+}
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 a43c68a08f5d..3445aca8a37b 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
@@ -129,3 +129,19 @@ now also stops the route.
Routes that relied on steps after these processors running for unauthenticated
requests must be
restructured. The authenticated paths are unchanged: a successfully
authenticated request continues
through the rest of the route exactly as before, and `OAuthLogoutProcessor` is
unchanged.
+
+=== camel-netty-http
+
+The security-constraint lookup now strips the endpoint context-path from the
request target
+case-insensitively, matching how consumer dispatch already matches it
+(`RestConsumerContextPathMatcher` compares with `equalsIgnoreCase` and a
lower-cased prefix).
+
+Previously the strip was guarded by a case-sensitive `startsWith`, so a
request whose context-path
+differed only by case was evaluated against the unstripped target. With
`matchOnUriPrefix=true` and
+a `securityConstraint` whose inclusions are specific sub-paths rather than a
catch-all, such a
+request could match no inclusion — and an unmatched target counts as
unrestricted — while still
+being dispatched to the route.
+
+Requests that differ from the configured context-path only by case are
therefore now subject to the
+same constraint as the exact-case form. Deployments that relied on the
previous behaviour to reach a
+route without a challenge will now receive `401`.