This is an automated email from the ASF dual-hosted git repository.
reta pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/cxf.git
The following commit(s) were added to refs/heads/main by this push:
new 5647d064819 reject WEB-INF and META-INF paths in AbstractHTTPServlet
(#3626)
5647d064819 is described below
commit 5647d0648194622d6fd67bb7b942813f2ad731db
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]>
---
.../cxf/transport/servlet/AbstractHTTPServlet.java | 30 +++
.../transport/servlet/AbstractHTTPServletTest.java | 203 +++++++++++++++++++++
.../cxf/systest/servlet/AbstractJettyServer.java | 7 +-
.../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, 375 insertions(+), 2 deletions(-)
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 8f59fe7d73b..7d8b5e9dbac 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));
@@ -365,6 +373,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");
@@ -392,8 +403,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..108699b2fbb
--- /dev/null
+++
b/rt/transports/http/src/test/java/org/apache/cxf/transport/servlet/AbstractHTTPServletTest.java
@@ -0,0 +1,203 @@
+/**
+ * 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 jakarta.servlet.FilterChain;
+import jakarta.servlet.RequestDispatcher;
+import jakarta.servlet.ServletConfig;
+import jakarta.servlet.ServletContext;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletOutputStream;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.ServletResponse;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.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(jakarta.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 7caec4bf463..a45c5e27cbf 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
@@ -53,8 +53,11 @@ public abstract class AbstractJettyServer extends
AbstractBusTestServerBase {
context.setContextPath(contextPath);
context.setBaseResource(ResourceFactory.of(new ContextHandler()).
newClassPathResource(resourcePath));
- context.setDescriptor(ResourceFactory.of(new ContextHandler()).
-
newClassPathResource(descriptor).getURI().toString());
+
+ if (descriptor != null) {
+ context.setDescriptor(ResourceFactory.of(new ContextHandler()).
+ newClassPathResource(descriptor).getURI().toString());
+ }
Handler.Collection handlers = new Handler.Sequence();
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"?>