This is an automated email from the ASF dual-hosted git repository. rzo1 pushed a commit to branch tomee-10.x in repository https://gitbox.apache.org/repos/asf/tomee.git
commit eab9972abf2e3da6d303e140665717ab999521ef Author: Markus Jung <[email protected]> AuthorDate: Sat Aug 29 21:45:58 2026 +0200 RouterValve don't blindly forward (cherry picked from commit ebf823c0ce7440003a0a2f7702471bb590417af2) --- .../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")); + } +}
