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"));
+ }
+}