CVE-2023-26119: Insufficient Information (NVD-CWE-noinfo)
value: Versions of the package net.sourceforge.htmlunit:htmlunit from 0 and before 3.0.0 are vulnerable to Remote Code Execution (RCE) via XSTL, when browsing the attacker’s webpage.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
CVSS v3 base score: 9.8
Published: 2023-04-03T05:15Z
Repository: https://github.com/HtmlUnit/htmlunit
Language: Java
Files changed: src/changes/changes.xml, src/main/java/com/gargoylesoftware/htmlunit/javascript/host/xml/XSLTProcessor.java, src/test/java/com/gargoylesoftware/htmlunit/ErrorOutputChecker.java, src/test/java/com/gargoylesoftware/htmlunit/javascript/host/xml/XSLTProcessorTest.java
Commit date: 2023-01-24 14:00:17 +0100
Commit message: enable FEATURE_SECURE_PROCESSING for the XSLT processor

Diff: diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 5369d05222..a945c33320 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -6,10 +6,13 @@
         <title>Changes</title>
     </properties>
 
     <body>
         <release version="2.70.0" date="Febuary xx, 2023" description="Bugfixes">
+            <action type="fix" dev="rbri">
+                Enable FEATURE_SECURE_PROCESSING for the XSLT processor.
+            </action>
             <action type="add" dev="rbri">
                 Document disabling of website certificate check in the FAQ.
             </action>
         </release>
 
diff --git a/src/main/java/com/gargoylesoftware/htmlunit/javascript/host/xml/XSLTProcessor.java b/src/main/java/com/gargoylesoftware/htmlunit/javascript/host/xml/XSLTProcessor.java
index c22c87579e..171226c1a1 100644
--- a/src/main/java/com/gargoylesoftware/htmlunit/javascript/host/xml/XSLTProcessor.java
+++ b/src/main/java/com/gargoylesoftware/htmlunit/javascript/host/xml/XSLTProcessor.java
@@ -24,10 +24,11 @@ import java.io.ByteArrayOutputStream;
 import java.io.StringWriter;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
 
+import javax.xml.XMLConstants;
 import javax.xml.parsers.DocumentBuilderFactory;
 import javax.xml.transform.OutputKeys;
 import javax.xml.transform.Result;
 import javax.xml.transform.Source;
 import javax.xml.transform.Transformer;
@@ -126,10 +127,15 @@ public class XSLTProcessor extends HtmlUnitScriptable {
             final DomNode xsltDomNode = style_.getDomNodeOrDie();
             final Source xsltSource = new DOMSource(xsltDomNode);
 
             final TransformerFactory transformerFactory = TransformerFactory.newInstance();
 
+            // By default, the JDK turns on FSP for DOM and SAX parsers and XML schema validators,
+            // which sets a number of processing limits on the processors. Conversely, by default,
+            // the JDK turns off FSP for transformers and XPath, which enables extension functions for XSLT and XPath.
+            transformerFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
+
             final SgmlPage page = sourceDomNode.getPage();
             if (page != null && page.getWebClient().getBrowserVersion()
                                             .hasFeature(JS_XSLT_TRANSFORM_INDENT)) {
                 final DomNode outputNode = findOutputNode(xsltDomNode);
                 if (outputNode != null) {
diff --git a/src/test/java/com/gargoylesoftware/htmlunit/ErrorOutputChecker.java b/src/test/java/com/gargoylesoftware/htmlunit/ErrorOutputChecker.java
index 2540ec24b2..8b4e1da7f3 100644
--- a/src/test/java/com/gargoylesoftware/htmlunit/ErrorOutputChecker.java
+++ b/src/test/java/com/gargoylesoftware/htmlunit/ErrorOutputChecker.java
@@ -45,11 +45,16 @@ public class ErrorOutputChecker implements MethodRule {
             Pattern.compile("SLF4J: .*\r?\n"),
 
             // Quercus
             Pattern.compile(".*com.caucho.quercus.servlet.QuercusServlet initImpl\r?\n"),
             Pattern.compile(".*QuercusServlet starting as QuercusServletImpl\r?\n"),
-            Pattern.compile(".*Quercus finished initialization in \\d*ms\r?\n")
+            Pattern.compile(".*Quercus finished initialization in \\d*ms\r?\n"),
+
+            // Xalan
+            Pattern.compile("ERROR:\\s*'Use of the extension function "
+                    + "'http://xml\\.apache\\.org/xalan/java/.*' "
+                    + "is not allowed when the secure processing feature is set to true\\.'\r?\n"),
     };
 
     /**
      * {@inheritDoc}
      */
diff --git a/src/test/java/com/gargoylesoftware/htmlunit/javascript/host/xml/XSLTProcessorTest.java b/src/test/java/com/gargoylesoftware/htmlunit/javascript/host/xml/XSLTProcessorTest.java
index 4020d2783e..43c37d983f 100644
--- a/src/test/java/com/gargoylesoftware/htmlunit/javascript/host/xml/XSLTProcessorTest.java
+++ b/src/test/java/com/gargoylesoftware/htmlunit/javascript/host/xml/XSLTProcessorTest.java
@@ -21,10 +21,11 @@ import org.junit.runner.RunWith;
 
 import com.gargoylesoftware.htmlunit.MockWebConnection;
 import com.gargoylesoftware.htmlunit.WebDriverTestCase;
 import com.gargoylesoftware.htmlunit.junit.BrowserRunner;
 import com.gargoylesoftware.htmlunit.junit.BrowserRunner.Alerts;
+import com.gargoylesoftware.htmlunit.junit.BrowserRunner.HtmlUnitNYI;
 import com.gargoylesoftware.htmlunit.util.MimeType;
 
 /**
  * Tests for {@link XSLTProcessor}.
  *
@@ -36,32 +37,53 @@ public class XSLTProcessorTest extends WebDriverTestCase {
 
     /**
      * @throws Exception if the test fails
      */
     @Test
-    @Alerts("exception")
+    @Alerts(DEFAULT = "<html xmlns=\"http://www.w3.org/1999/xhtml\">"
+                + "<head></head><body> <h2>My CD Collection</h2> "
+                + "<ul><li>Empire Burlesque (Bob Dylan)</li></ul> </body></html>",
+            FF = "<html xmlns=\"http://www.w3.org/1999/xhtml\">"
+                + "<body><h2>My CD Collection</h2>"
+                + "<ul><li>Empire Burlesque (Bob Dylan)</li></ul></body></html>",
+            FF_ESR = "<html xmlns=\"http://www.w3.org/1999/xhtml\">"
+                + "<body><h2>My CD Collection</h2>"
+                + "<ul><li>Empire Burlesque (Bob Dylan)</li></ul></body></html>",
+            IE = "exception")
+    @HtmlUnitNYI(CHROME = "<html><body><h2>My CD Collection</h2>"
+                + "<ul><li>Empire Burlesque (Bob Dylan)</li></ul></body></html>",
+            EDGE = "<html><body><h2>My CD Collection</h2>"
+                + "<ul><li>Empire Burlesque (Bob Dylan)</li></ul></body></html>",
+            FF = "<html><body><h2>My CD Collection</h2>"
+                + "<ul><li>Empire Burlesque (Bob Dylan)</li></ul></body></html>",
+            FF_ESR = "<html><body><h2>My CD Collection</h2>"
+                + "<ul><li>Empire Burlesque (Bob Dylan)</li></ul></body></html>")
     public void test() throws Exception {
         final String html = "<html><head>\n"
             + "<script>\n"
             + LOG_TITLE_FUNCTION
+
+            + "  function createXmlDocument() {\n"
+            + "    return document.implementation.createDocument('', '', null);\n"
+            + "  }\n"
+
+            + "  function  loadXMLDocument(url) {\n"
+            + "    var xhttp = new XMLHttpRequest();\n"
+            + "    xhttp.open('GET', url, false);\n"
+            + "    xhttp.send();\n"
+            + "    return xhttp.responseXML;\n"
+            + "  }"
+
             + "  function test() {\n"
             + "    try {\n"
-            + "      var xmlDoc = createXmlDocument();\n"
-            + "      xmlDoc.async = false;\n"
-            + "      xmlDoc.load('" + URL_SECOND + "1');\n"
-
-            + "      var xslDoc;\n"
-            + "      xslDoc = createXmlDocument();\n"
-            + "      xslDoc.async = false;\n"
-            + "      xslDoc.load('" + URL_SECOND + "2');\n"
+            + "      var xmlDoc = loadXMLDocument('" + URL_SECOND + "1');\n"
+            + "      var xslDoc = loadXMLDocument('" + URL_SECOND + "2');\n"
 
             + "      var processor = new XSLTProcessor();\n"
             + "      processor.importStylesheet(xslDoc);\n"
             + "      var newDocument = processor.transformToDocument(xmlDoc);\n"
-            + "      log(new XMLSerializer().serializeToString(newDocument.documentElement).length);\n"
-            + "      newDocument = processor.transformToDocument(xmlDoc.documentElement);\n"
-            + "      log(newDocument.documentElement);\n"
+            + "      log(new XMLSerializer().serializeToString(newDocument.documentElement));\n"
             + "    } catch(e) { log('exception'); }\n"
             + "  }\n"
 
             + "  function createXmlDocument() {\n"
             + "    return document.implementation.createDocument('', '', null);\n"
@@ -197,6 +219,70 @@ public class XSLTProcessorTest extends WebDriverTestCase {
             + "</head>\n"
             + "<body onload='test()'>\n"
             + "</body></html>";
         loadPageVerifyTitle2(html);
     }
+
+    /**
+     * @throws Exception if the test fails
+     */
+    @Test
+    @Alerts(DEFAULT = {"preparation done", "null"},
+            FF = {"preparation done", "exception"},
+            FF_ESR = {"preparation done", "exception"},
+            IE = "exception")
+    @HtmlUnitNYI(CHROME = {"preparation done", "exception"},
+            EDGE = {"preparation done", "exception"})
+    public void testSecurity() throws Exception {
+        final String html = "<html><head>\n"
+            + "<script>\n"
+            + LOG_TITLE_FUNCTION
+
+            + "  function createXmlDocument() {\n"
+            + "    return document.implementation.createDocument('', '', null);\n"
+            + "  }\n"
+
+            + "  function  loadXMLDocument(url) {\n"
+            + "    var xhttp = new XMLHttpRequest();\n"
+            + "    xhttp.open('GET', url, false);\n"
+            + "    xhttp.send();\n"
+            + "    return xhttp.responseXML;\n"
+            + "  }"
+
+            + "  function test() {\n"
+            + "    try {\n"
+            + "      var xmlDoc = loadXMLDocument('" + URL_SECOND + "1');\n"
+            + "      var xslDoc = loadXMLDocument('" + URL_SECOND + "2');\n"
+
+            + "      var processor = new XSLTProcessor();\n"
+            + "      processor.importStylesheet(xslDoc);\n"
+            + "      log('preparation done');\n"
+            + "      var newDocument = processor.transformToDocument(xmlDoc);\n"
+            + "      log(newDocument);\n"
+            + "    } catch(e) { log('exception'); }\n"
+            + "  }\n"
+            + "</script></head>"
+            + "<body onload='test()'>\n"
+            + "</body></html>";
+
+        final String xml
+            = "<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?>\n"
+            + "

";
+
+        final String xsl
+            = " <xsl:stylesheet version=\"1.0\" xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\" "
+                    + "xmlns:rt=\"http://xml.apache.org/xalan/java/java.lang.Runtime\" "
+                    + "xmlns:ob=\"http://xml.apache.org/xalan/java/java.lang.Object\">\r\n"
+            + "    <xsl:template match='/'>\n"
+            + "      <xsl:variable name='rtobject' select='rt:getRuntime()'/>\n"
+            + "      <xsl:variable name=\"rtString\" select=\"ob:toString($rtobject)\"/>\n"
+            + "      <xsl:value-of select=\"$rtString\"/>\n"
+            + "    </xsl:template>\r\n"
+            + "  </xsl:stylesheet>";
+
+        final MockWebConnection conn = getMockWebConnection();
+        conn.setResponse(new URL(URL_SECOND, "1"), xml, MimeType.TEXT_XML);
+        conn.setResponse(new URL(URL_SECOND, "2"), xsl, MimeType.TEXT_XML);
+
+        loadPageVerifyTitle2(html);
+    }
 }

Vulnerable code:             Pattern.compile(".*Quercus finished initialization in \\d*ms\r?\n")
    @Alerts("exception")
            + "      var xmlDoc = createXmlDocument();\n"
            + "      xmlDoc.async = false;\n"
            + "      xmlDoc.load('" + URL_SECOND + "1');\n"

            + "      var xslDoc;\n"
            + "      xslDoc = createXmlDocument();\n"
            + "      xslDoc.async = false;\n"
            + "      xslDoc.load('" + URL_SECOND + "2');\n"
            + "      log(new XMLSerializer().serializeToString(newDocument.documentElement).length);\n"
            + "      newDocument = processor.transformToDocument(xmlDoc.documentElement);\n"
            + "      log(newDocument.documentElement);\n"
Fixed code:             <action type="fix" dev="rbri">
                Enable FEATURE_SECURE_PROCESSING for the XSLT processor.
            </action>
import javax.xml.XMLConstants;
            // By default, the JDK turns on FSP for DOM and SAX parsers and XML schema validators,
            // which sets a number of processing limits on the processors. Conversely, by default,
            // the JDK turns off FSP for transformers and XPath, which enables extension functions for XSLT and XPath.
            transformerFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);

            Pattern.compile(".*Quercus finished initialization in \\d*ms\r?\n"),

            // Xalan
            Pattern.compile("ERROR:\\s*'Use of the extension function "
                    + "'http://xml\\.apache\\.org/xalan/java/.*' "
                    + "is not allowed when the secure processing feature is set to true\\.'\r?\n"),
import com.gargoylesoftware.htmlunit.junit.BrowserRunner.HtmlUnitNYI;
    @Alerts(DEFAULT = "<html xmlns=\"http://www.w3.org/1999/xhtml\">"
                + "<head></head><body> <h2>My CD Collection</h2> "
                + "<ul><li>Empire Burlesque (Bob Dylan)</li></ul> </body></html>",
            FF = "<html xmlns=\"http://www.w3.org/1999/xhtml\">"
                + "<body><h2>My CD Collection</h2>"
                + "<ul><li>Empire Burlesque (Bob Dylan)</li></ul></body></html>",
            FF_ESR = "<html xmlns=\"http://www.w3.org/1999/xhtml\">"
                + "<body><h2>My CD Collection</h2>"
                + "<ul><li>Empire Burlesque (Bob Dylan)</li></ul></body></html>",
            IE = "exception")
    @HtmlUnitNYI(CHROME = "<html><body><h2>My CD Collection</h2>"
                + "<ul><li>Empire Burlesque (Bob Dylan)</li></ul></body></html>",
            EDGE = "<html><body><h2>My CD Collection</h2>"
                + "<ul><li>Empire Burlesque (Bob Dylan)</li></ul></body></html>",
            FF = "<html><body><h2>My CD Collection</h2>"
                + "<ul><li>Empire Burlesque (Bob Dylan)</li></ul></body></html>",
            FF_ESR = "<html><body><h2>My CD Collection</h2>"
                + "<ul><li>Empire Burlesque (Bob Dylan)</li></ul></body></html>")

            + "  function createXmlDocument() {\n"
            + "    return document.implementation.createDocument('', '', null);\n"
            + "  }\n"

            + "  function  loadXMLDocument(url) {\n"
            + "    var xhttp = new XMLHttpRequest();\n"
            + "    xhttp.open('GET', url, false);\n"
            + "    xhttp.send();\n"
            + "    return xhttp.responseXML;\n"
            + "  }"

            + "      var xmlDoc = loadXMLDocument('" + URL_SECOND + "1');\n"
            + "      var xslDoc = loadXMLDocument('" + URL_SECOND + "2');\n"
            + "      log(new XMLSerializer().serializeToString(newDocument.documentElement));\n"

    /**
     * @throws Exception if the test fails
     */
    @Test
    @Alerts(DEFAULT = {"preparation done", "null"},
            FF = {"preparation done", "exception"},
            FF_ESR = {"preparation done", "exception"},
            IE = "exception")
    @HtmlUnitNYI(CHROME = {"preparation done", "exception"},
            EDGE = {"preparation done", "exception"})
    public void testSecurity() throws Exception {
        final String html = "<html><head>\n"
            + "<script>\n"
            + LOG_TITLE_FUNCTION

            + "  function createXmlDocument() {\n"
            + "    return document.implementation.createDocument('', '', null);\n"
            + "  }\n"

            + "  function  loadXMLDocument(url) {\n"
            + "    var xhttp = new XMLHttpRequest();\n"
            + "    xhttp.open('GET', url, false);\n"
            + "    xhttp.send();\n"
            + "    return xhttp.responseXML;\n"
            + "  }"

            + "  function test() {\n"
            + "    try {\n"
            + "      var xmlDoc = loadXMLDocument('" + URL_SECOND + "1');\n"
            + "      var xslDoc = loadXMLDocument('" + URL_SECOND + "2');\n"

            + "      var processor = new XSLTProcessor();\n"
            + "      processor.importStylesheet(xslDoc);\n"
            + "      log('preparation done');\n"
            + "      var newDocument = processor.transformToDocument(xmlDoc);\n"
            + "      log(newDocument);\n"
            + "    } catch(e) { log('exception'); }\n"
            + "  }\n"
            + "</script></head>"
            + "<body onload='test()'>\n"
            + "</body></html>";

        final String xml
            = "<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?>\n"
            + "

";

        final String xsl
            = " <xsl:stylesheet version=\"1.0\" xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\" "
                    + "xmlns:rt=\"http://xml.apache.org/xalan/java/java.lang.Runtime\" "
                    + "xmlns:ob=\"http://xml.apache.org/xalan/java/java.lang.Object\">\r\n"
            + "    <xsl:template match='/'>\n"
            + "      <xsl:variable name='rtobject' select='rt:getRuntime()'/>\n"
            + "      <xsl:variable name=\"rtString\" select=\"ob:toString($rtobject)\"/>\n"
            + "      <xsl:value-of select=\"$rtString\"/>\n"
            + "    </xsl:template>\r\n"
            + "  </xsl:stylesheet>";

        final MockWebConnection conn = getMockWebConnection();
        conn.setResponse(new URL(URL_SECOND, "1"), xml, MimeType.TEXT_XML);
        conn.setResponse(new URL(URL_SECOND, "2"), xsl, MimeType.TEXT_XML);

        loadPageVerifyTitle2(html);
    }