From 6d424d7edaab0d281c97d0b91a625feee8449c32 Mon Sep 17 00:00:00 2001 From: "David M. Johnson" Date: Sat, 29 Aug 2026 10:31:30 -0400 Subject: [PATCH 1/2] Disable vendor extension types on the XML-RPC servlet Roller's XML-RPC API uses only the standard XML-RPC value types; the library's vendor extension types are unused and are switched off. The endpoint is also gated on webservices.enableXmlRpc so a disabled service is not reachable and does not read request bodies. Adds XmlRpcExtensionTypeTest, which checks the shipped configuration and that ordinary calls still parse. Claude-Session: https://claude.ai/code/session_01A1fhY1E2PCFU6UAPXu2WtV --- .../ui/core/filters/XmlRpcEnabledFilter.java | 66 +++++++++++ app/src/main/webapp/WEB-INF/web.xml | 14 ++- .../xmlrpc/XmlRpcExtensionTypeTest.java | 103 ++++++++++++++++++ 3 files changed, 182 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/org/apache/roller/weblogger/ui/core/filters/XmlRpcEnabledFilter.java create mode 100644 app/src/test/java/org/apache/roller/weblogger/webservices/xmlrpc/XmlRpcExtensionTypeTest.java diff --git a/app/src/main/java/org/apache/roller/weblogger/ui/core/filters/XmlRpcEnabledFilter.java b/app/src/main/java/org/apache/roller/weblogger/ui/core/filters/XmlRpcEnabledFilter.java new file mode 100644 index 000000000..e99fd4f9a --- /dev/null +++ b/app/src/main/java/org/apache/roller/weblogger/ui/core/filters/XmlRpcEnabledFilter.java @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. 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. For additional information regarding + * copyright in this work, please see the NOTICE file in the top level + * directory of this distribution. + */ + +package org.apache.roller.weblogger.ui.core.filters; + +import java.io.IOException; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.roller.weblogger.config.WebloggerRuntimeConfig; + +/** + * Gates the XML-RPC endpoint on webservices.enableXmlRpc, ahead of + * the servlet, so a disabled service is not reachable. + */ +public class XmlRpcEnabledFilter implements Filter { + + private static final Log LOG = LogFactory.getLog(XmlRpcEnabledFilter.class); + + @Override + public void init(FilterConfig filterConfig) { + // nothing to configure + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, + FilterChain chain) throws IOException, ServletException { + + if (!WebloggerRuntimeConfig.getBooleanProperty("webservices.enableXmlRpc")) { + if (LOG.isDebugEnabled()) { + LOG.debug("XML-RPC service is disabled; rejecting request"); + } + ((HttpServletResponse) response).sendError(HttpServletResponse.SC_NOT_FOUND); + return; + } + + chain.doFilter(request, response); + } + + @Override + public void destroy() { + // nothing to release + } +} diff --git a/app/src/main/webapp/WEB-INF/web.xml b/app/src/main/webapp/WEB-INF/web.xml index 0418832da..63f53f062 100644 --- a/app/src/main/webapp/WEB-INF/web.xml +++ b/app/src/main/webapp/WEB-INF/web.xml @@ -45,6 +45,11 @@ org.apache.roller.weblogger.ui.core.filters.BootstrapFilter + + XmlRpcEnabledFilter + org.apache.roller.weblogger.ui.core.filters.XmlRpcEnabledFilter + + RequestMappingFilter org.apache.roller.weblogger.ui.rendering.filters.RequestMappingFilter @@ -154,6 +159,11 @@ + + XmlRpcEnabledFilter + /roller-services/xmlrpc + + RequestMappingFilter /* @@ -296,9 +306,11 @@ Sets whether the servlet supports vendor extensions for XML-RPC. + Roller uses only the standard XML-RPC value types; the library's + vendor extension types are unused and are switched off. enabledForExtensions - true + false diff --git a/app/src/test/java/org/apache/roller/weblogger/webservices/xmlrpc/XmlRpcExtensionTypeTest.java b/app/src/test/java/org/apache/roller/weblogger/webservices/xmlrpc/XmlRpcExtensionTypeTest.java new file mode 100644 index 000000000..848a44d77 --- /dev/null +++ b/app/src/test/java/org/apache/roller/weblogger/webservices/xmlrpc/XmlRpcExtensionTypeTest.java @@ -0,0 +1,103 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. 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. For additional information regarding + * copyright in this work, please see the NOTICE file in the top level + * directory of this distribution. + */ +package org.apache.roller.weblogger.webservices.xmlrpc; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.apache.xmlrpc.parser.XmlRpcRequestParser; +import org.apache.xmlrpc.common.XmlRpcHttpRequestConfigImpl; +import org.apache.xmlrpc.common.TypeFactoryImpl; +import org.apache.xmlrpc.common.XmlRpcController; +import org.apache.xmlrpc.server.XmlRpcServer; +import org.junit.jupiter.api.Test; +import org.xml.sax.InputSource; +import org.xml.sax.XMLReader; +import org.xml.sax.helpers.XMLReaderFactory; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Covers how the XML-RPC endpoint treats vendor extension types. + * + *

Roller's XML-RPC API uses only the standard value types, so the library's + * vendor extension types are switched off in web.xml and the + * endpoint is closed entirely while the service is disabled. + */ +public class XmlRpcExtensionTypeTest { + + private static final Path WEB_XML = + Paths.get("src", "main", "webapp", "WEB-INF", "web.xml"); + + private String parseRequest(String xml, boolean extensionsEnabled) throws Exception { + XmlRpcServer server = new XmlRpcServer(); + XmlRpcHttpRequestConfigImpl config = new XmlRpcHttpRequestConfigImpl(); + config.setEnabledForExtensions(extensionsEnabled); + + XmlRpcRequestParser parser = new XmlRpcRequestParser( + config, new TypeFactoryImpl((XmlRpcController) server)); + XMLReader reader = XMLReaderFactory.createXMLReader(); + reader.setContentHandler(parser); + reader.parse(new InputSource( + new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)))); + return parser.getMethodName(); + } + + private static final String ORDINARY_REQUEST = + "" + + "blogger.getUsersBlogs" + + "hello" + + ""; + + /** The shipped configuration must not enable the extension types. */ + @Test + public void shippedConfigurationDisablesExtensionTypes() throws Exception { + String webXml = new String(Files.readAllBytes(WEB_XML), StandardCharsets.UTF_8); + int idx = webXml.indexOf("enabledForExtensions"); + assertTrue(idx > 0, "enabledForExtensions param not found in web.xml"); + String tail = webXml.substring(idx, Math.min(idx + 300, webXml.length())); + assertTrue(tail.contains("false"), + "the XML-RPC servlet must not enable vendor extension types:\n" + tail); + } + + /** The endpoint is closed by a filter while the service is switched off. */ + @Test + public void endpointIsGatedWhileTheServiceIsDisabled() throws Exception { + String webXml = new String(Files.readAllBytes(WEB_XML), StandardCharsets.UTF_8); + assertTrue(webXml.contains("XmlRpcEnabledFilter"), + "a filter must gate the XML-RPC endpoint"); + int mapping = webXml.indexOf("XmlRpcEnabledFilter", + webXml.indexOf("")); + assertTrue(mapping > 0, "the gating filter must be mapped"); + assertTrue(webXml.indexOf("/roller-services/xmlrpc", mapping) > 0, + "the gating filter must be mapped to the XML-RPC endpoint"); + } + + /** Ordinary XML-RPC calls must still parse with extensions disabled. */ + @Test + public void ordinaryCallsStillParseWithExtensionsDisabled() throws Exception { + String methodName = parseRequest(ORDINARY_REQUEST, false); + assertNotNull(methodName, "an ordinary XML-RPC call must still parse"); + assertEquals("blogger.getUsersBlogs", methodName); + } +} \ No newline at end of file From 44e7064c0bd261c4266c703d988fcbf0ed36df74 Mon Sep 17 00:00:00 2001 From: "David M. Johnson" Date: Tue, 1 Sep 2026 16:07:37 -0400 Subject: [PATCH 2/2] Improve XML-RPC endpoint gating tests and responses --- .../ui/core/filters/XmlRpcEnabledFilter.java | 9 +- app/src/main/webapp/WEB-INF/web.xml | 3 + .../xmlrpc/XmlRpcExtensionTypeTest.java | 115 +++++++++++++++--- 3 files changed, 104 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/org/apache/roller/weblogger/ui/core/filters/XmlRpcEnabledFilter.java b/app/src/main/java/org/apache/roller/weblogger/ui/core/filters/XmlRpcEnabledFilter.java index e99fd4f9a..3d5255bbc 100644 --- a/app/src/main/java/org/apache/roller/weblogger/ui/core/filters/XmlRpcEnabledFilter.java +++ b/app/src/main/java/org/apache/roller/weblogger/ui/core/filters/XmlRpcEnabledFilter.java @@ -49,10 +49,11 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { if (!WebloggerRuntimeConfig.getBooleanProperty("webservices.enableXmlRpc")) { - if (LOG.isDebugEnabled()) { - LOG.debug("XML-RPC service is disabled; rejecting request"); - } - ((HttpServletResponse) response).sendError(HttpServletResponse.SC_NOT_FOUND); + LOG.warn("XML-RPC service is disabled; rejecting request"); + HttpServletResponse httpResponse = (HttpServletResponse) response; + httpResponse.setStatus(HttpServletResponse.SC_NOT_FOUND); + httpResponse.setContentType("text/plain;charset=UTF-8"); + httpResponse.getWriter().write("XML-RPC service is disabled"); return; } diff --git a/app/src/main/webapp/WEB-INF/web.xml b/app/src/main/webapp/WEB-INF/web.xml index 63f53f062..bc7ce1f69 100644 --- a/app/src/main/webapp/WEB-INF/web.xml +++ b/app/src/main/webapp/WEB-INF/web.xml @@ -162,6 +162,9 @@ XmlRpcEnabledFilter /roller-services/xmlrpc + REQUEST + FORWARD + INCLUDE diff --git a/app/src/test/java/org/apache/roller/weblogger/webservices/xmlrpc/XmlRpcExtensionTypeTest.java b/app/src/test/java/org/apache/roller/weblogger/webservices/xmlrpc/XmlRpcExtensionTypeTest.java index 848a44d77..36a5928e0 100644 --- a/app/src/test/java/org/apache/roller/weblogger/webservices/xmlrpc/XmlRpcExtensionTypeTest.java +++ b/app/src/test/java/org/apache/roller/weblogger/webservices/xmlrpc/XmlRpcExtensionTypeTest.java @@ -18,24 +18,40 @@ package org.apache.roller.weblogger.webservices.xmlrpc; import java.io.ByteArrayInputStream; +import java.io.StringWriter; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import javax.servlet.FilterChain; +import javax.servlet.ServletRequest; +import javax.servlet.http.HttpServletResponse; import org.apache.xmlrpc.parser.XmlRpcRequestParser; import org.apache.xmlrpc.common.XmlRpcHttpRequestConfigImpl; import org.apache.xmlrpc.common.TypeFactoryImpl; import org.apache.xmlrpc.common.XmlRpcController; import org.apache.xmlrpc.server.XmlRpcServer; +import org.apache.roller.weblogger.config.WebloggerRuntimeConfig; +import org.apache.roller.weblogger.ui.core.filters.XmlRpcEnabledFilter; import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; import org.xml.sax.InputSource; import org.xml.sax.XMLReader; -import org.xml.sax.helpers.XMLReaderFactory; +import javax.xml.parsers.DocumentBuilderFactory; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.mockStatic; /** * Covers how the XML-RPC endpoint treats vendor extension types. @@ -46,8 +62,9 @@ */ public class XmlRpcExtensionTypeTest { - private static final Path WEB_XML = - Paths.get("src", "main", "webapp", "WEB-INF", "web.xml"); + private static final Path WEB_XML = Paths.get( + System.getProperty("project.basedir", System.getProperty("user.dir")), + "src", "main", "webapp", "WEB-INF", "web.xml"); private String parseRequest(String xml, boolean extensionsEnabled) throws Exception { XmlRpcServer server = new XmlRpcServer(); @@ -56,7 +73,10 @@ private String parseRequest(String xml, boolean extensionsEnabled) throws Except XmlRpcRequestParser parser = new XmlRpcRequestParser( config, new TypeFactoryImpl((XmlRpcController) server)); - XMLReader reader = XMLReaderFactory.createXMLReader(); + javax.xml.parsers.SAXParserFactory saxFactory = + javax.xml.parsers.SAXParserFactory.newInstance(); + saxFactory.setNamespaceAware(true); + XMLReader reader = saxFactory.newSAXParser().getXMLReader(); reader.setContentHandler(parser); reader.parse(new InputSource( new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)))); @@ -69,28 +89,62 @@ private String parseRequest(String xml, boolean extensionsEnabled) throws Except + "hello" + ""; + private static final String EXTENSION_REQUEST = + "" + + "blogger.getUsersBlogs" + + "" + + ""; + + private Document webXml() throws Exception { + try (java.io.InputStream in = Files.newInputStream(WEB_XML)) { + assertNotNull(in, "web.xml must be available in the Maven build directory"); + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(true); + return factory.newDocumentBuilder().parse(in); + } + } + /** The shipped configuration must not enable the extension types. */ @Test public void shippedConfigurationDisablesExtensionTypes() throws Exception { - String webXml = new String(Files.readAllBytes(WEB_XML), StandardCharsets.UTF_8); - int idx = webXml.indexOf("enabledForExtensions"); - assertTrue(idx > 0, "enabledForExtensions param not found in web.xml"); - String tail = webXml.substring(idx, Math.min(idx + 300, webXml.length())); - assertTrue(tail.contains("false"), - "the XML-RPC servlet must not enable vendor extension types:\n" + tail); + Document document = webXml(); + NodeList params = document.getElementsByTagNameNS("*", "init-param"); + boolean found = false; + for (int i = 0; i < params.getLength(); i++) { + Element param = (Element) params.item(i); + NodeList names = param.getElementsByTagNameNS("*", "param-name"); + if (names.getLength() > 0 && "enabledForExtensions".equals(names.item(0).getTextContent().trim())) { + found = true; + NodeList values = param.getElementsByTagNameNS("*", "param-value"); + assertEquals("false", values.item(0).getTextContent().trim()); + } + } + assertTrue(found, "enabledForExtensions init-param not found in web.xml"); } /** The endpoint is closed by a filter while the service is switched off. */ @Test public void endpointIsGatedWhileTheServiceIsDisabled() throws Exception { - String webXml = new String(Files.readAllBytes(WEB_XML), StandardCharsets.UTF_8); - assertTrue(webXml.contains("XmlRpcEnabledFilter"), - "a filter must gate the XML-RPC endpoint"); - int mapping = webXml.indexOf("XmlRpcEnabledFilter", - webXml.indexOf("")); - assertTrue(mapping > 0, "the gating filter must be mapped"); - assertTrue(webXml.indexOf("/roller-services/xmlrpc", mapping) > 0, - "the gating filter must be mapped to the XML-RPC endpoint"); + Document document = webXml(); + NodeList mappings = document.getElementsByTagNameNS("*", "filter-mapping"); + boolean found = false; + for (int i = 0; i < mappings.getLength(); i++) { + Element mapping = (Element) mappings.item(i); + if ("XmlRpcEnabledFilter".equals(mapping.getElementsByTagNameNS("*", "filter-name") + .item(0).getTextContent().trim())) { + found = true; + assertEquals("/roller-services/xmlrpc", + mapping.getElementsByTagNameNS("*", "url-pattern").item(0) + .getTextContent().trim()); + java.util.Set dispatchers = new java.util.HashSet<>(); + NodeList nodes = mapping.getElementsByTagNameNS("*", "dispatcher"); + for (int j = 0; j < nodes.getLength(); j++) { + dispatchers.add(nodes.item(j).getTextContent().trim()); + } + assertEquals(java.util.Set.of("REQUEST", "FORWARD", "INCLUDE"), dispatchers); + } + } + assertTrue(found, "the XML-RPC filter mapping was not found"); } /** Ordinary XML-RPC calls must still parse with extensions disabled. */ @@ -100,4 +154,27 @@ public void ordinaryCallsStillParseWithExtensionsDisabled() throws Exception { assertNotNull(methodName, "an ordinary XML-RPC call must still parse"); assertEquals("blogger.getUsersBlogs", methodName); } -} \ No newline at end of file + + /** Vendor extension values must be rejected by the shipped parser policy. */ + @Test + public void extensionValuesAreRejectedWhenDisabled() { + assertThrows(Exception.class, () -> parseRequest(EXTENSION_REQUEST, false)); + } + + /** Disabled requests receive a non-HTML response and never reach the chain. */ + @Test + public void disabledFilterReturnsPlainResponse() throws Exception { + HttpServletResponse response = mock(HttpServletResponse.class); + FilterChain chain = mock(FilterChain.class); + when(response.getWriter()).thenReturn(new java.io.PrintWriter(new StringWriter())); + try (MockedStatic config = mockStatic(WebloggerRuntimeConfig.class)) { + config.when(() -> WebloggerRuntimeConfig.getBooleanProperty("webservices.enableXmlRpc")) + .thenReturn(false); + new XmlRpcEnabledFilter().doFilter(mock(ServletRequest.class), response, chain); + } + verify(response).setStatus(HttpServletResponse.SC_NOT_FOUND); + verify(response).setContentType("text/plain;charset=UTF-8"); + verify(response).getWriter(); + verifyNoInteractions(chain); + } +}