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

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


The following commit(s) were added to refs/heads/main by this push:
     new ebf823c0ce RouterValve don't blindly forward
ebf823c0ce is described below

commit ebf823c0ce7440003a0a2f7702471bb590417af2
Author: Markus Jung <[email protected]>
AuthorDate: Sat Aug 29 21:45:58 2026 +0200

    RouterValve don't blindly forward
---
 .../apache/tomee/catalina/routing/RouterValve.java | 162 +++++++++++++++++++++
 .../routing/RouterValveDestinationTest.java        |  70 +++++++++
 2 files changed, 232 insertions(+)

diff --git 
a/tomee/tomee-catalina/src/main/java/org/apache/tomee/catalina/routing/RouterValve.java
 
b/tomee/tomee-catalina/src/main/java/org/apache/tomee/catalina/routing/RouterValve.java
index 87c8a155c3..72c1f087f6 100644
--- 
a/tomee/tomee-catalina/src/main/java/org/apache/tomee/catalina/routing/RouterValve.java
+++ 
b/tomee/tomee-catalina/src/main/java/org/apache/tomee/catalina/routing/RouterValve.java
@@ -17,19 +17,31 @@
 
 package org.apache.tomee.catalina.routing;
 
+import org.apache.catalina.Context;
 import org.apache.catalina.LifecycleException;
+import org.apache.catalina.Realm;
 import org.apache.catalina.connector.Request;
 import org.apache.catalina.connector.Response;
+import org.apache.catalina.util.RequestUtil;
 import org.apache.catalina.valves.ValveBase;
 import org.apache.openejb.config.DeploymentLoader;
 import org.apache.openejb.loader.SystemInstance;
+import org.apache.tomcat.util.buf.UDecoder;
+import org.apache.tomcat.util.descriptor.web.SecurityCollection;
+import org.apache.tomcat.util.descriptor.web.SecurityConstraint;
 
 import jakarta.servlet.ServletContext;
 import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletResponse;
 import java.io.File;
 import java.io.IOException;
 import java.net.MalformedURLException;
 import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.security.Principal;
+import java.util.Locale;
+
+import static org.apache.tomcat.util.http.RequestUtil.normalize;
 
 public class RouterValve extends ValveBase {
     public static final String ROUTER_CONF = "tomee-router.conf";
@@ -46,12 +58,162 @@ public class RouterValve extends ValveBase {
         }
 
         if (router.hasPrefix()) {
+            final String normalized = 
normalizeDestination(request.getContext(), destination);
+            if (normalized == null) {
+                response.sendError(HttpServletResponse.SC_NOT_FOUND);
+                return;
+            }
+            if (!isDestinationAuthorized(request, normalized)) {
+                response.sendError(HttpServletResponse.SC_FORBIDDEN);
+                return;
+            }
             request.getRequestDispatcher(destination).forward(request, 
response);
         } else {
             response.sendRedirect(destination);
         }
     }
 
+    /**
+     * Applies the same path processing as {@code 
ApplicationContext#getRequestDispatcher} and only
+     * accepts destinations a client could request directly.
+     *
+     * @return the normalized destination path or {@code null} if it is not 
forwardable
+     */
+    static String normalizeDestination(final Context context, final String 
destination) {
+        if (!destination.startsWith("/")) {
+            return null;
+        }
+
+        String path = destination;
+        final int query = path.indexOf('?');
+        if (query >= 0) {
+            path = path.substring(0, query);
+        }
+
+        path = RequestUtil.stripPathParams(path, null);
+
+        if (context == null || context.getDispatchersUseEncodedPaths()) {
+            try {
+                path = context == null
+                        ? UDecoder.URLDecode(path, StandardCharsets.UTF_8)
+                        : UDecoder.URLDecode(path, StandardCharsets.UTF_8,
+                            context.getEncodedSolidusHandlingEnum(), 
context.getEncodedReverseSolidusHandlingEnum());
+            } catch (final IllegalArgumentException iae) {
+                return null;
+            }
+        }
+
+        path = normalize(path);
+        if (path == null) {
+            return null;
+        }
+
+        final String upper = path.toUpperCase(Locale.ENGLISH);
+        if (upper.equals("/WEB-INF") || upper.startsWith("/WEB-INF/")
+                || upper.equals("/META-INF") || 
upper.startsWith("/META-INF/")) {
+            return null;
+        }
+
+        return path;
+    }
+
+    /**
+     * Evaluates the context security constraints matching the forward 
destination against the current principal.
+     */
+    private boolean isDestinationAuthorized(final Request request, final 
String path) {
+        final Context context = request.getContext();
+        if (context == null) {
+            return true;
+        }
+
+        final SecurityConstraint[] constraints = context.findConstraints();
+        if (constraints == null) {
+            return true;
+        }
+
+        for (final SecurityConstraint constraint : constraints) {
+            if (!constraint.getAuthConstraint() || 
!constraintMatches(constraint, path)) {
+                continue;
+            }
+
+            final Principal principal = request.getPrincipal();
+            if (principal == null) {
+                return false;
+            }
+            if (constraint.getAuthenticatedUsers()) { // "**"
+                continue;
+            }
+
+            final String[] roles = constraint.getAllRoles() ? 
context.findSecurityRoles() : constraint.findAuthRoles();
+            if (roles == null || roles.length == 0) { // auth-constraint 
without role: denies everybody
+                return false;
+            }
+
+            final Realm realm = context.getRealm();
+            if (realm == null) {
+                return false;
+            }
+
+            boolean allowed = false;
+            for (final String role : roles) {
+                if (realm.hasRole(request.getWrapper(), principal, role)) {
+                    allowed = true;
+                    break;
+                }
+            }
+            if (!allowed) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    private static boolean constraintMatches(final SecurityConstraint 
constraint, final String path) {
+        for (final SecurityCollection collection : 
constraint.findCollections()) {
+            for (final String pattern : collection.findPatterns()) {
+                if (patternMatches(path, pattern)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    // url-pattern matching as done for security constraints (servlet spec 
13.8.3)
+    static boolean patternMatches(final String path, final String pattern) {
+        if (pattern == null || pattern.isEmpty()) {
+            return false;
+        }
+        if (pattern.equals(path) || "/".equals(pattern)) {
+            return true;
+        }
+        if (pattern.startsWith("/") && pattern.endsWith("/*")) {
+            final String prefix = pattern.substring(0, pattern.length() - 2);
+            if (prefix.isEmpty()) {
+                return true;
+            }
+            String current = path;
+            while (true) {
+                if (current.equals(prefix)) {
+                    return true;
+                }
+                final int slash = current.lastIndexOf('/');
+                if (slash <= 0) {
+                    return false;
+                }
+                current = current.substring(0, slash);
+            }
+        }
+        if (pattern.startsWith("*.")) {
+            final int slash = path.lastIndexOf('/');
+            final int period = path.lastIndexOf('.');
+            return slash >= 0 && period > slash && path.length() > period + 1
+                    && pattern.substring(2).equals(path.substring(period + 1));
+        }
+        return false;
+    }
+
     public void setConfigurationPath(final URL configurationPath) {
         router.readConfiguration(configurationPath);
     }
diff --git 
a/tomee/tomee-catalina/src/test/java/org/apache/tomee/catalina/routing/RouterValveDestinationTest.java
 
b/tomee/tomee-catalina/src/test/java/org/apache/tomee/catalina/routing/RouterValveDestinationTest.java
new file mode 100644
index 0000000000..94c1d9d4c6
--- /dev/null
+++ 
b/tomee/tomee-catalina/src/test/java/org/apache/tomee/catalina/routing/RouterValveDestinationTest.java
@@ -0,0 +1,70 @@
+/*
+ * 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.tomee.catalina.routing;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+public class RouterValveDestinationTest {
+    @Test
+    public void keepsRegularDestinations() {
+        assertEquals("/app/page", RouterValve.normalizeDestination(null, 
"/app/page"));
+        assertEquals("/app/page", RouterValve.normalizeDestination(null, 
"/app/page?x=1"));
+        assertEquals("/app/a b", RouterValve.normalizeDestination(null, 
"/app/a%20b"));
+        assertEquals("/b", RouterValve.normalizeDestination(null, "/a/../b"));
+    }
+
+    @Test
+    public void refusesWebInfAndMetaInf() {
+        assertNull(RouterValve.normalizeDestination(null, "/WEB-INF/web.xml"));
+        assertNull(RouterValve.normalizeDestination(null, "/WEB-INF"));
+        assertNull(RouterValve.normalizeDestination(null, 
"/web-inf/classes/app/DBConfig.class"));
+        assertNull(RouterValve.normalizeDestination(null, 
"/META-INF/context.xml"));
+        // traversal into WEB-INF, raw and encoded
+        assertNull(RouterValve.normalizeDestination(null, 
"/pub/../WEB-INF/web.xml"));
+        assertNull(RouterValve.normalizeDestination(null, 
"/pub/%2e%2e/WEB-INF/web.xml"));
+        assertNull(RouterValve.normalizeDestination(null, 
"/%57EB-INF/web.xml"));
+        // path parameters must not hide a segment from the check
+        assertNull(RouterValve.normalizeDestination(null, 
"/WEB-INF;x=y/web.xml"));
+    }
+
+    @Test
+    public void refusesEscapesAndRelativePaths() {
+        assertNull(RouterValve.normalizeDestination(null, "/.."));
+        assertNull(RouterValve.normalizeDestination(null, "/../outside"));
+        assertNull(RouterValve.normalizeDestination(null, "relative/path"));
+        assertNull(RouterValve.normalizeDestination(null, "/bad%zzescape"));
+    }
+
+    @Test
+    public void securityConstraintPatternMatching() {
+        assertTrue(RouterValve.patternMatches("/admin/users", "/admin/*"));
+        assertTrue(RouterValve.patternMatches("/admin", "/admin/*"));
+        assertTrue(RouterValve.patternMatches("/admin/users", "/admin/users"));
+        assertTrue(RouterValve.patternMatches("/anything", "/*"));
+        assertTrue(RouterValve.patternMatches("/anything", "/"));
+        assertTrue(RouterValve.patternMatches("/a/b.jsp", "*.jsp"));
+        assertFalse(RouterValve.patternMatches("/admins", "/admin/*"));
+        assertFalse(RouterValve.patternMatches("/public/x", "/admin/*"));
+        assertFalse(RouterValve.patternMatches("/a/b.jspx", "*.jsp"));
+    }
+}

Reply via email to