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

reta pushed a commit to branch 3.6.x-fixes
in repository https://gitbox.apache.org/repos/asf/cxf.git

commit 9a1d94af8ef41a15c84e1ef4af609fa666d0b40f
Author: Naveed Khan <[email protected]>
AuthorDate: Tue Oct 27 04:41:23 2026 +0530

    reject WEB-INF and META-INF paths in AbstractHTTPServlet (#3626)
    
    * reject WEB-INF and META-INF paths in AbstractHTTPServlet
    
    * make protected static paths configurable via 
static-resources-protected-path-list
    
    * Add integration tests
    
    ---------
    
    Co-authored-by: Andriy Redko <[email protected]>
    (cherry picked from commit 5647d0648194622d6fd67bb7b942813f2ad731db)
---
 .../cxf/transport/servlet/AbstractHTTPServlet.java |  30 +++
 .../transport/servlet/AbstractHTTPServletTest.java | 204 +++++++++++++++++++++
 .../cxf/systest/servlet/AbstractJettyServer.java   |   4 +-
 .../servlet/CXFStaticResourcesServletTest.java     |  99 ++++++++++
 .../org/apache/cxf/systest/static/WEB-INF/web.xml  |  37 ++++
 .../org/apache/cxf/systest/static/test.xml         |   1 +
 6 files changed, 374 insertions(+), 1 deletion(-)

diff --git 
a/rt/transports/http/src/main/java/org/apache/cxf/transport/servlet/AbstractHTTPServlet.java
 
b/rt/transports/http/src/main/java/org/apache/cxf/transport/servlet/AbstractHTTPServlet.java
index 5eb95651d8d..0e6eadb3000 100644
--- 
a/rt/transports/http/src/main/java/org/apache/cxf/transport/servlet/AbstractHTTPServlet.java
+++ 
b/rt/transports/http/src/main/java/org/apache/cxf/transport/servlet/AbstractHTTPServlet.java
@@ -72,6 +72,10 @@ public abstract class AbstractHTTPServlet extends 
HttpServlet implements Filter
         Arrays.asList(new String[]{"POST", "GET", "PUT", "DELETE", "HEAD", 
"OPTIONS", "TRACE"});
 
     private static final String STATIC_RESOURCES_PARAMETER = 
"static-resources-list";
+    private static final String STATIC_RESOURCES_PROTECTED_PATHS_PARAMETER =
+        "static-resources-protected-path-list";
+    private static final String DEFAULT_STATIC_RESOURCES_PROTECTED_PATHS =
+        "(?i)/WEB-INF/.* (?i)/META-INF/.*";
     private static final String STATIC_WELCOME_FILE_PARAMETER = 
"static-welcome-file";
     private static final String STATIC_CACHE_CONTROL = "static-cache-control";
     private static final String STATIC_RESOURCES_MAP_RESOURCE = 
"/cxfServletStaticResourcesMap.txt";
@@ -102,6 +106,7 @@ public abstract class AbstractHTTPServlet extends 
HttpServlet implements Filter
     }
     
     private List<Pattern> staticResourcesList;
+    private List<Pattern> staticResourcesProtectedPathList;
     private String staticWelcomeFile;
     private List<Pattern> redirectList;
     private String dispatcherServletPath;
@@ -116,6 +121,9 @@ public abstract class AbstractHTTPServlet extends 
HttpServlet implements Filter
         super.init(servletConfig);
 
         staticResourcesList = 
parseListSequence(servletConfig.getInitParameter(STATIC_RESOURCES_PARAMETER));
+        String protectedPaths = 
servletConfig.getInitParameter(STATIC_RESOURCES_PROTECTED_PATHS_PARAMETER);
+        staticResourcesProtectedPathList = parseListSequence(protectedPaths != 
null
+            ? protectedPaths : DEFAULT_STATIC_RESOURCES_PROTECTED_PATHS);
         staticWelcomeFile = 
servletConfig.getInitParameter(STATIC_WELCOME_FILE_PARAMETER);
         redirectList = 
parseListSequence(servletConfig.getInitParameter(REDIRECTS_PARAMETER));
         redirectQueryCheck = 
Boolean.valueOf(servletConfig.getInitParameter(REDIRECT_QUERY_CHECK_PARAMETER));
@@ -353,6 +361,9 @@ public abstract class AbstractHTTPServlet extends 
HttpServlet implements Filter
     protected void serveStaticContent(HttpServletRequest request,
                                       HttpServletResponse response,
                                       String pathInfo) throws ServletException 
{
+        if (isProtectedPath(pathInfo)) {
+            throw new ServletException("Static resource " + pathInfo + " is 
not available");
+        }
         try (InputStream is = getResourceAsStream(pathInfo)) {
             if (is == null) {
                 throw new ServletException("Static resource " + pathInfo + " 
is not available");
@@ -380,8 +391,27 @@ public abstract class AbstractHTTPServlet extends 
HttpServlet implements Filter
         return staticContentTypes.get(extension);
     }
 
+    /**
+     * The container never serves WEB-INF or META-INF directly to a client, 
but a RequestDispatcher
+     * and ServletContext.getResourceAsStream can reach them, so a request 
path matching
+     * static-resources-protected-path-list is not forwarded or served as 
static content.
+     */
+    private boolean isProtectedPath(String path) {
+        if (path != null && staticResourcesProtectedPathList != null) {
+            for (Pattern pattern : staticResourcesProtectedPathList) {
+                if (pattern.matcher(path).matches()) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
     protected void redirect(HttpServletRequest request, HttpServletResponse 
response, String pathInfo)
         throws ServletException {
+        if (isProtectedPath(pathInfo)) {
+            throw new ServletException("No RequestDispatcher can be created 
for path " + pathInfo);
+        }
         boolean customServletPath = dispatcherServletPath != null;
         String theServletPath = customServletPath ? dispatcherServletPath : 
"/";
 
diff --git 
a/rt/transports/http/src/test/java/org/apache/cxf/transport/servlet/AbstractHTTPServletTest.java
 
b/rt/transports/http/src/test/java/org/apache/cxf/transport/servlet/AbstractHTTPServletTest.java
new file mode 100644
index 00000000000..5d389e59e35
--- /dev/null
+++ 
b/rt/transports/http/src/test/java/org/apache/cxf/transport/servlet/AbstractHTTPServletTest.java
@@ -0,0 +1,204 @@
+/**
+ * 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.cxf.transport.servlet;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+import javax.servlet.FilterChain;
+import javax.servlet.RequestDispatcher;
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+import javax.servlet.ServletOutputStream;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.cxf.Bus;
+import org.apache.cxf.resource.DefaultResourceManager;
+import org.apache.cxf.resource.ResourceManager;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class AbstractHTTPServletTest {
+
+    private static final String WEB_XML = "<web-app/>";
+
+    @Test
+    public void testStaticResourceUnderWebInfIsNotServed() throws Exception {
+        ServletContext sc = mockServletContext("/WEB-INF/web.xml", WEB_XML);
+        TestServlet servlet = initServlet(sc, "static-resources-list", 
"/.*\\.xml");
+        HttpServletResponse response = mock(HttpServletResponse.class);
+        ByteArrayOutputStream bos = captureOutput(response);
+
+        assertThrows(ServletException.class, () -> 
servlet.doGet(mockRequest("/WEB-INF/web.xml"), response));
+
+        verify(sc, never()).getResourceAsStream(anyString());
+        assertEquals("", bos.toString(StandardCharsets.UTF_8));
+        assertFalse(servlet.invoked);
+    }
+
+    @Test
+    public void testStaticResourceUnderMetaInfIsNotServed() throws Exception {
+        ServletContext sc = mockServletContext("/meta-inf/context.xml", 
WEB_XML);
+        TestServlet servlet = initServlet(sc, "static-resources-list", 
"/.*\\.xml");
+        HttpServletResponse response = mock(HttpServletResponse.class);
+        ByteArrayOutputStream bos = captureOutput(response);
+
+        assertThrows(ServletException.class,
+            () -> servlet.doGet(mockRequest("/meta-inf/context.xml"), 
response));
+
+        verify(sc, never()).getResourceAsStream(anyString());
+        assertEquals("", bos.toString(StandardCharsets.UTF_8));
+    }
+
+    @Test
+    public void testCustomProtectedPathList() throws Exception {
+        ServletContext sc = mockServletContext("/WEB-INF/web.xml", WEB_XML);
+        ServletConfig config = mockServletConfig(sc, "static-resources-list", 
"/.*\\.xml");
+        
when(config.getInitParameter("static-resources-protected-path-list")).thenReturn("/private/.*");
+        TestServlet servlet = new TestServlet(sc);
+        servlet.init(config);
+
+        assertThrows(ServletException.class, () -> 
servlet.doGet(mockRequest("/private/a.xml"),
+                                                                  
mock(HttpServletResponse.class)));
+
+        HttpServletResponse response = mock(HttpServletResponse.class);
+        ByteArrayOutputStream bos = captureOutput(response);
+        servlet.doGet(mockRequest("/WEB-INF/web.xml"), response);
+        assertEquals(WEB_XML, bos.toString(StandardCharsets.UTF_8));
+    }
+
+    @Test
+    public void testStaticResourceOutsideWebInfIsServed() throws Exception {
+        ServletContext sc = mockServletContext("/index.xml", WEB_XML);
+        TestServlet servlet = initServlet(sc, "static-resources-list", 
"/.*\\.xml");
+        HttpServletResponse response = mock(HttpServletResponse.class);
+        ByteArrayOutputStream bos = captureOutput(response);
+
+        servlet.doGet(mockRequest("/index.xml"), response);
+
+        assertEquals(WEB_XML, bos.toString(StandardCharsets.UTF_8));
+    }
+
+    @Test
+    public void testRedirectUnderWebInfIsNotForwarded() throws Exception {
+        ServletContext sc = mock(ServletContext.class);
+        RequestDispatcher rd = mock(RequestDispatcher.class);
+        when(sc.getRequestDispatcher(anyString())).thenReturn(rd);
+        TestServlet servlet = initServlet(sc, "redirects-list", "/.*\\.jsp");
+        HttpServletResponse response = mock(HttpServletResponse.class);
+
+        assertThrows(ServletException.class,
+            () -> servlet.doGet(mockRequest("/WEB-INF/views/admin.jsp"), 
response));
+
+        verify(sc, never()).getRequestDispatcher(anyString());
+        assertFalse(servlet.invoked);
+    }
+
+    private static ServletContext mockServletContext(String path, String 
content) {
+        ServletContext sc = mock(ServletContext.class);
+        when(sc.getResourceAsStream(path))
+            .thenReturn(new 
ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)));
+        return sc;
+    }
+
+    private static TestServlet initServlet(ServletContext sc, String 
paramName, String paramValue)
+        throws ServletException {
+        TestServlet servlet = new TestServlet(sc);
+        servlet.init(mockServletConfig(sc, paramName, paramValue));
+        return servlet;
+    }
+
+    private static ServletConfig mockServletConfig(ServletContext sc, String 
paramName, String paramValue) {
+        ServletConfig config = mock(ServletConfig.class);
+        when(config.getServletContext()).thenReturn(sc);
+        when(config.getInitParameter(paramName)).thenReturn(paramValue);
+        if (!"redirects-list".equals(paramName)) {
+            
when(config.getInitParameter("redirect-servlet-path")).thenReturn(null);
+        } else {
+            
when(config.getInitParameter("redirect-servlet-path")).thenReturn("/");
+        }
+        return config;
+    }
+
+    private static HttpServletRequest mockRequest(String pathInfo) {
+        HttpServletRequest request = mock(HttpServletRequest.class);
+        when(request.getPathInfo()).thenReturn(pathInfo);
+        when(request.getMethod()).thenReturn("GET");
+        return request;
+    }
+
+    private static ByteArrayOutputStream captureOutput(HttpServletResponse 
response) throws IOException {
+        final ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        when(response.getOutputStream()).thenReturn(new ServletOutputStream() {
+            @Override
+            public void write(int b) {
+                bos.write(b);
+            }
+            @Override
+            public boolean isReady() {
+                return true;
+            }
+            @Override
+            public void setWriteListener(javax.servlet.WriteListener 
writeListener) {
+            }
+        });
+        return bos;
+    }
+
+    private static final class TestServlet extends AbstractHTTPServlet {
+        private static final long serialVersionUID = 1L;
+        private final Bus bus;
+        private boolean invoked;
+
+        TestServlet(ServletContext sc) {
+            bus = mock(Bus.class);
+            when(bus.getExtension(ResourceManager.class))
+                .thenReturn(new DefaultResourceManager(new 
ServletContextResourceResolver(sc)));
+        }
+
+        @Override
+        protected Bus getBus() {
+            return bus;
+        }
+
+        @Override
+        protected void invoke(HttpServletRequest request, HttpServletResponse 
response) {
+            invoked = true;
+        }
+
+        @Override
+        public void doFilter(ServletRequest request, ServletResponse response, 
FilterChain chain) {
+        }
+    }
+}
diff --git 
a/systests/transports/src/test/java/org/apache/cxf/systest/servlet/AbstractJettyServer.java
 
b/systests/transports/src/test/java/org/apache/cxf/systest/servlet/AbstractJettyServer.java
index 2d72777d717..7f015a002fd 100644
--- 
a/systests/transports/src/test/java/org/apache/cxf/systest/servlet/AbstractJettyServer.java
+++ 
b/systests/transports/src/test/java/org/apache/cxf/systest/servlet/AbstractJettyServer.java
@@ -51,7 +51,9 @@ public abstract class AbstractJettyServer extends 
AbstractBusTestServerBase {
             final WebAppContext context = new WebAppContext();
             context.setContextPath(contextPath);
             
context.setBaseResource(Resource.newClassPathResource(resourcePath));
-            
context.setDescriptor(Resource.newClassPathResource(descriptor).getURI().toString());
+            if (descriptor != null) { 
+                
context.setDescriptor(Resource.newClassPathResource(descriptor).getURI().toString());
+            }
 
             HandlerCollection handlers = new HandlerCollection();
             handlers.setHandlers(new Handler[] {context, new 
DefaultHandler()});
diff --git 
a/systests/transports/src/test/java/org/apache/cxf/systest/servlet/CXFStaticResourcesServletTest.java
 
b/systests/transports/src/test/java/org/apache/cxf/systest/servlet/CXFStaticResourcesServletTest.java
new file mode 100644
index 00000000000..83659cfcd26
--- /dev/null
+++ 
b/systests/transports/src/test/java/org/apache/cxf/systest/servlet/CXFStaticResourcesServletTest.java
@@ -0,0 +1,99 @@
+/**
+ * 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.cxf.systest.servlet;
+
+import java.nio.charset.StandardCharsets;
+
+import org.apache.cxf.jaxrs.model.AbstractResourceInfo;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.CloseableHttpClient;
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+public class CXFStaticResourcesServletTest extends AbstractServletTest {
+
+    @Ignore
+    public static class CXFStaticServletEmbeddedJettyServer extends 
AbstractJettyServer {
+        public static final int PORT = 
allocatePortAsInt(CXFStaticServletEmbeddedJettyServer.class);
+
+        public CXFStaticServletEmbeddedJettyServer() {
+            super(null, "/org/apache/cxf/systest/static/", CONTEXT, PORT);
+        }
+    }
+
+    @BeforeClass
+    public static void startServers() throws Exception {
+        AbstractResourceInfo.clearAllMaps();
+        assertTrue("server did not launch correctly", 
launchServer(CXFStaticServletEmbeddedJettyServer.class, true));
+        createStaticBus();
+    }
+
+    @AfterClass
+    public static void tearDown() throws Exception {
+        stopAllServers();
+    }
+
+    @Test
+    public void testGetStaticResource() throws Exception {
+        final String expected = new String(getClass()
+            .getResourceAsStream("/org/apache/cxf/systest/static/test.xml")
+            .readAllBytes(), StandardCharsets.UTF_8);
+
+        try (CloseableHttpClient client = newClient()) {
+            final HttpGet method = new HttpGet(uri("/services/test.xml"));
+            try (CloseableHttpResponse res = client.execute(method)) {
+                final String doc = new 
String(res.getEntity().getContent().readAllBytes(), StandardCharsets.UTF_8);
+                assertThat(doc, equalTo(expected));
+            }
+        }
+    }
+
+    @Test
+    public void testGetNonExistingStaticResource() throws Exception {
+        try (CloseableHttpClient client = newClient()) {
+            final HttpGet method = new 
HttpGet(uri("/services/WEB-INF/beans.xml"));
+            try (CloseableHttpResponse res = client.execute(method)) {
+                assertThat(res.getStatusLine().getStatusCode(), equalTo(500));
+            }
+        }
+    }
+
+    @Test
+    public void testGetProtectedStaticResource() throws Exception {
+        try (CloseableHttpClient client = newClient()) {
+            final HttpGet method = new 
HttpGet(uri("/services/WEB-INF/web.xml"));
+            try (CloseableHttpResponse res = client.execute(method)) {
+                assertThat(res.getStatusLine().getStatusCode(), equalTo(500));
+            }
+        }
+    }
+
+    @Override
+    protected int getPort() {
+        return CXFStaticServletEmbeddedJettyServer.PORT;
+    }
+}
diff --git 
a/systests/transports/src/test/resources/org/apache/cxf/systest/static/WEB-INF/web.xml
 
b/systests/transports/src/test/resources/org/apache/cxf/systest/static/WEB-INF/web.xml
new file mode 100644
index 00000000000..2cd0018283e
--- /dev/null
+++ 
b/systests/transports/src/test/resources/org/apache/cxf/systest/static/WEB-INF/web.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 
2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd";>
+<!--
+  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.
+-->
+<web-app>
+    <servlet>
+        <servlet-name>CXFServlet</servlet-name>
+        <display-name>CXF Servlet</display-name>
+        <servlet-class>
+        org.apache.cxf.transport.servlet.CXFServlet
+    </servlet-class>
+        <init-param>
+            <param-name>static-resources-list</param-name>
+            <param-value>.*.xml</param-value>
+        </init-param>
+    </servlet>
+    <servlet-mapping>
+        <servlet-name>CXFServlet</servlet-name>
+        <url-pattern>/services/*</url-pattern>
+    </servlet-mapping>
+</web-app>
diff --git 
a/systests/transports/src/test/resources/org/apache/cxf/systest/static/test.xml 
b/systests/transports/src/test/resources/org/apache/cxf/systest/static/test.xml
new file mode 100644
index 00000000000..89a325edfb1
--- /dev/null
+++ 
b/systests/transports/src/test/resources/org/apache/cxf/systest/static/test.xml
@@ -0,0 +1 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>

Reply via email to