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`.

Reply via email to