garydgregory commented on code in PR #44:
URL: https://github.com/apache/commons-xml/pull/44#discussion_r3852955416


##########
src/main/java/org/apache/commons/xml/HardeningXPath.java:
##########
@@ -0,0 +1,141 @@
+/*
+ * 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
+ *
+ *      https://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.commons.xml;
+
+import java.io.IOException;
+
+import javax.xml.namespace.NamespaceContext;
+import javax.xml.namespace.QName;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.xpath.XPath;
+import javax.xml.xpath.XPathExpression;
+import javax.xml.xpath.XPathExpressionException;
+import javax.xml.xpath.XPathFunctionResolver;
+import javax.xml.xpath.XPathVariableResolver;
+
+import org.w3c.dom.Document;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+
+/**
+ * {@link XPath} wrapper that performs the document build behind every {@link 
InputSource}-taking {@code evaluate} call with a hardened, namespace-aware
+ * {@link javax.xml.parsers.DocumentBuilder} and evaluates the delegate 
against the parsed {@link Document}, so the engine's own parser never runs.
+ *
+ * <p>The JAXP contract for {@link XPath#evaluate(String, InputSource, QName)} 
is "build a document from the source, then evaluate against it", and both the
+ * stock JDK and Apache Xalan provision an internal parser for that build 
which {@link javax.xml.XMLConstants#FEATURE_SECURE_PROCESSING} on the
+ * {@link javax.xml.xpath.XPathFactory} does not reach. Parsing here puts the 
build on the library's resolver floor: an external reference inside the document
+ * resolves to empty content, so it is neither fetched nor leaked, and the 
evaluation proceeds on whatever the parse produced. {@link #compile(String)} 
wraps
+ * the compiled expression in a {@link HardeningXPathExpression} on the same 
terms.</p>
+ *
+ * <p>The {@code evaluateExpression} default methods added to the interface by 
Java 9 route through the {@code evaluate} overloads overridden here, so they
+ * carry the same rewrite on newer runtimes even though this class targets 
Java 8.</p>
+ */
+final class HardeningXPath implements XPath {
+
+    /**
+     * Parses the source through a hardened, namespace-aware {@link 
javax.xml.parsers.DocumentBuilder}, mirroring the namespace awareness of the 
parser the
+     * engine would have provisioned.
+     *
+     * @param source The document to evaluate against.
+     * @return The parsed document.
+     * @throws NullPointerException     if {@code source} is {@code null}, per 
the {@link XPath} contract.
+     * @throws XPathExpressionException if the source cannot be parsed.
+     */
+    static Document parse(final InputSource source) throws 
XPathExpressionException {
+        if (source == null) {
+            throw new NullPointerException("source cannot be null");
+        }
+        try {
+            final DocumentBuilderFactory factory = 
DocumentBuilderHardener.harden(DocumentBuilderFactory.newInstance());
+            factory.setNamespaceAware(true);
+            return factory.newDocumentBuilder().parse(source);
+        } catch (final ParserConfigurationException | SAXException | 
IOException e) {
+            throw new XPathExpressionException(e);
+        }
+    }
+
+    private final XPath delegate;
+
+    HardeningXPath(final XPath delegate) {
+        this.delegate = delegate;
+    }
+
+    @Override
+    public String evaluate(final String expression, final InputSource source) 
throws XPathExpressionException {
+        return delegate.evaluate(expression, parse(source));
+    }
+
+    @Override
+    public Object evaluate(final String expression, final InputSource source, 
final QName returnType) throws XPathExpressionException {
+        return delegate.evaluate(expression, parse(source), returnType);
+    }
+
+    @Override
+    public XPathExpression compile(final String expression) throws 
XPathExpressionException {
+        final XPathExpression compiled = delegate.compile(expression);
+        return compiled == null ? null : new 
HardeningXPathExpression(compiled);
+    }
+
+    // <editor-fold defaultstate="collapsed" desc="Trivial delegation">
+    @Override
+    public String evaluate(final String expression, final Object item) throws 
XPathExpressionException {
+        return delegate.evaluate(expression, item);
+    }
+
+    @Override
+    public Object evaluate(final String expression, final Object item, final 
QName returnType) throws XPathExpressionException {
+        return delegate.evaluate(expression, item, returnType);
+    }
+
+    @Override
+    public NamespaceContext getNamespaceContext() {
+        return delegate.getNamespaceContext();
+    }
+
+    @Override
+    public XPathFunctionResolver getXPathFunctionResolver() {
+        return delegate.getXPathFunctionResolver();
+    }
+
+    @Override
+    public XPathVariableResolver getXPathVariableResolver() {
+        return delegate.getXPathVariableResolver();
+    }
+
+    @Override
+    public void reset() {
+        delegate.reset();
+    }
+
+    @Override
+    public void setNamespaceContext(final NamespaceContext nsContext) {
+        delegate.setNamespaceContext(nsContext);
+    }
+
+    @Override
+    public void setXPathFunctionResolver(final XPathFunctionResolver resolver) 
{
+        delegate.setXPathFunctionResolver(resolver);
+    }
+
+    @Override
+    public void setXPathVariableResolver(final XPathVariableResolver resolver) 
{
+        delegate.setXPathVariableResolver(resolver);
+    }
+    // </editor-fold>

Review Comment:
   Remove this comment noise please.



##########
src/main/java/org/apache/commons/xml/HardeningXPath.java:
##########
@@ -0,0 +1,141 @@
+/*
+ * 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
+ *
+ *      https://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.commons.xml;
+
+import java.io.IOException;
+
+import javax.xml.namespace.NamespaceContext;
+import javax.xml.namespace.QName;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.xpath.XPath;
+import javax.xml.xpath.XPathExpression;
+import javax.xml.xpath.XPathExpressionException;
+import javax.xml.xpath.XPathFunctionResolver;
+import javax.xml.xpath.XPathVariableResolver;
+
+import org.w3c.dom.Document;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+
+/**
+ * {@link XPath} wrapper that performs the document build behind every {@link 
InputSource}-taking {@code evaluate} call with a hardened, namespace-aware
+ * {@link javax.xml.parsers.DocumentBuilder} and evaluates the delegate 
against the parsed {@link Document}, so the engine's own parser never runs.
+ *
+ * <p>The JAXP contract for {@link XPath#evaluate(String, InputSource, QName)} 
is "build a document from the source, then evaluate against it", and both the
+ * stock JDK and Apache Xalan provision an internal parser for that build 
which {@link javax.xml.XMLConstants#FEATURE_SECURE_PROCESSING} on the
+ * {@link javax.xml.xpath.XPathFactory} does not reach. Parsing here puts the 
build on the library's resolver floor: an external reference inside the document
+ * resolves to empty content, so it is neither fetched nor leaked, and the 
evaluation proceeds on whatever the parse produced. {@link #compile(String)} 
wraps
+ * the compiled expression in a {@link HardeningXPathExpression} on the same 
terms.</p>
+ *
+ * <p>The {@code evaluateExpression} default methods added to the interface by 
Java 9 route through the {@code evaluate} overloads overridden here, so they
+ * carry the same rewrite on newer runtimes even though this class targets 
Java 8.</p>
+ */
+final class HardeningXPath implements XPath {
+
+    /**
+     * Parses the source through a hardened, namespace-aware {@link 
javax.xml.parsers.DocumentBuilder}, mirroring the namespace awareness of the 
parser the
+     * engine would have provisioned.
+     *
+     * @param source The document to evaluate against.
+     * @return The parsed document.
+     * @throws NullPointerException     if {@code source} is {@code null}, per 
the {@link XPath} contract.
+     * @throws XPathExpressionException if the source cannot be parsed.
+     */
+    static Document parse(final InputSource source) throws 
XPathExpressionException {
+        if (source == null) {
+            throw new NullPointerException("source cannot be null");
+        }
+        try {
+            final DocumentBuilderFactory factory = 
DocumentBuilderHardener.harden(DocumentBuilderFactory.newInstance());
+            factory.setNamespaceAware(true);
+            return factory.newDocumentBuilder().parse(source);
+        } catch (final ParserConfigurationException | SAXException | 
IOException e) {
+            throw new XPathExpressionException(e);
+        }
+    }
+
+    private final XPath delegate;
+
+    HardeningXPath(final XPath delegate) {
+        this.delegate = delegate;
+    }
+
+    @Override
+    public String evaluate(final String expression, final InputSource source) 
throws XPathExpressionException {
+        return delegate.evaluate(expression, parse(source));
+    }
+
+    @Override
+    public Object evaluate(final String expression, final InputSource source, 
final QName returnType) throws XPathExpressionException {
+        return delegate.evaluate(expression, parse(source), returnType);
+    }
+
+    @Override
+    public XPathExpression compile(final String expression) throws 
XPathExpressionException {
+        final XPathExpression compiled = delegate.compile(expression);
+        return compiled == null ? null : new 
HardeningXPathExpression(compiled);
+    }
+
+    // <editor-fold defaultstate="collapsed" desc="Trivial delegation">

Review Comment:
   Remove this comment noise please.



##########
src/test/java/org/apache/commons/xml/ShadingFootprintTest.java:
##########
@@ -69,23 +70,25 @@ class ShadingFootprintTest {
     private static final Set<String> STAX_HARDENER = set("StaxHardener", 
"HardeningXMLInputFactory", "FallbackIgnoreXMLResolver", HARDENING_EXCEPTION);
 
     /**
-     * TrAX, XPath and schema re-harden their sub-parsers through {@link 
SAXParserHardener#harden(Source)}, so each builds on the full SAX closure below.
+     * TrAX, XPath and schema re-harden their sub-parsers through {@link 
SAXParserHardener#hardenSource(Source)}, so each builds on the full SAX closure 
below; XPath
+     * additionally parses InputSource-taking evaluate calls through the DOM 
hardener, so its closure carries that set too.
      */
     private static final Set<String> TRANSFORMER_HARDENER = 
saxParsersHardenerPlus("TransformerHardener", "HardeningTransformerFactory",
             "HardeningTransformer", "HardeningTransformerHandler", 
"HardeningTemplates", "HardeningTemplatesHandler", "HardeningXMLFilter",
             "FallbackIgnoreURIResolver", "SaxonProvider", "SaxonProvider$1", 
"SaxonProvider$HardenedConfiguration"
             , "SaxonProvider$SaxonProviderConfigurer");
 
-    private static final Set<String> XPATH_HARDENER = 
saxParsersHardenerPlus("XPathHardener", "FallbackIgnoreURIResolver", 
"SaxonProvider", "SaxonProvider$1",
-            "SaxonProvider$HardenedConfiguration", 
"SaxonProvider$SaxonProviderConfigurer");
+    private static final Set<String> XPATH_HARDENER = 
saxParsersHardenerPlus("XPathHardener", "FallbackIgnoreURIResolver", 
"SaxonProvider",
+            "SaxonProvider$1", "SaxonProvider$HardenedConfiguration", 
"SaxonProvider$SaxonProviderConfigurer", "HardeningXPathFactory", 
"HardeningXPath",
+            "HardeningXPathExpression", "DocumentBuilderHardener", 
"HardeningDocumentBuilder", "HardeningDocumentBuilderFactory");
 
     private static final Set<String> SCHEMA_HARDENER = 
saxParsersHardenerPlus("SchemaHardener", "HardeningSchemaFactory", 
"HardeningValidator",
             "HardeningValidatorHandler", "HardeningSchema", 
"FallbackIgnoreLSResourceResolver");
 
     /**
      * Only the public {@link XmlFactories} entry, which news up every 
hardener, still pulls the whole library; this is its class count.

Review Comment:
   What does "news up" means? Use plain English IMO.



##########
src/test/java/org/apache/commons/xml/XPathInputSourceTest.java:
##########
@@ -0,0 +1,88 @@
+/*
+ * 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
+ *
+ *      https://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.commons.xml;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import javax.xml.xpath.XPathExpressionException;
+import javax.xml.xpath.XPathFactory;
+
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Checks that the document parse behind {@code XPath.evaluate(String, 
InputSource)} (and its compiled {@code XPathExpression} counterpart) cannot 
pull in an
+ * external general entity.
+ *
+ * <p>The stock JDK and Apache Xalan implement the {@link 
org.xml.sax.InputSource}-taking {@code evaluate} entry points by provisioning 
an internal document
+ * parser that {@code FEATURE_SECURE_PROCESSING} on the {@link XPathFactory} 
does not reach. The {@link HardeningXPathFactory} wrapper parses the input
+ * through a hardened {@code DocumentBuilder} instead, so the external 
reference resolves to empty on the floor (or the parse is rejected outright), 
while the
+ * evaluation itself still works. Tagged {@code xpath}, so it runs under 
test-stockjdk, test-xalan and test-xalan-xerces; the Saxon engine takes the 
separate
+ * {@code SaxonProvider} path covered by {@code 
SaxonXPathExternalCallsTest}.</p>
+ */
+@Tag("xpath")
+class XPathInputSourceTest {
+
+    private static final String EXPRESSION = "string(/root/child)";
+
+    /** {@link AttackTestSupport#xmlBody} content whose single entity 
reference resolves to {@link AttackTestSupport#LEAKED_MARKER} if the DTD is 
fetched. */
+    private static String entityPayload() {
+        return "<?xml version=\"1.0\"?>\n"
+                + "<!DOCTYPE root [\n  <!ENTITY xxe SYSTEM \"" + 
AttackTestSupport.resourceUrl("referenced.txt") + "\">\n]>\n"
+                + AttackTestSupport.xmlBody("&xxe;");
+    }
+
+    @Test
+    void hardenedXPathEvaluateDoesNotLeak() throws Exception {
+        final String result;
+        try {
+            result = 
XmlFactories.newXPathFactory().newXPath().evaluate(EXPRESSION, 
AttackTestSupport.inputSource(entityPayload()));
+        } catch (final XPathExpressionException blocked) {
+            return; // Acceptable: the parse rejected the reference rather 
than resolving it to empty.

Review Comment:
   Same comment as in PR #42 
   Add a better comment or use `assertThrows()`



##########
src/main/java/org/apache/commons/xml/HardeningXPathFactory.java:
##########
@@ -0,0 +1,73 @@
+/*
+ * 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
+ *
+ *      https://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.commons.xml;
+
+import javax.xml.xpath.XPath;
+import javax.xml.xpath.XPathFactory;
+import javax.xml.xpath.XPathFactoryConfigurationException;
+import javax.xml.xpath.XPathFunctionResolver;
+import javax.xml.xpath.XPathVariableResolver;
+
+/**
+ * {@link XPathFactory} wrapper that returns a {@link HardeningXPath} from 
{@link #newXPath()}.
+ *
+ * <p>Required because {@link 
javax.xml.XMLConstants#FEATURE_SECURE_PROCESSING} on the factory governs only 
the XPath engine: the stock JDK and Apache Xalan
+ * implement the {@link org.xml.sax.InputSource}-taking {@code evaluate} entry 
points by provisioning an internal document parser the feature does not reach.
+ * The wrapper performs that document build itself through a hardened parser 
instead; see {@link HardeningXPath}.</p>
+ */
+final class HardeningXPathFactory extends XPathFactory {
+
+    private final XPathFactory delegate;
+
+    HardeningXPathFactory(final XPathFactory delegate) {
+        this.delegate = delegate;
+    }
+
+    @Override
+    public XPath newXPath() {
+        final XPath xpath = delegate.newXPath();
+        return xpath == null ? null : new HardeningXPath(xpath);
+    }
+
+    // <editor-fold defaultstate="collapsed" desc="Trivial delegation">
+    @Override
+    public boolean getFeature(final String name) throws 
XPathFactoryConfigurationException {
+        return delegate.getFeature(name);
+    }
+
+    @Override
+    public boolean isObjectModelSupported(final String objectModel) {
+        return delegate.isObjectModelSupported(objectModel);
+    }
+
+    @Override
+    public void setFeature(final String name, final boolean value) throws 
XPathFactoryConfigurationException {
+        delegate.setFeature(name, value);
+    }
+
+    @Override
+    public void setXPathFunctionResolver(final XPathFunctionResolver resolver) 
{
+        delegate.setXPathFunctionResolver(resolver);
+    }
+
+    @Override
+    public void setXPathVariableResolver(final XPathVariableResolver resolver) 
{
+        delegate.setXPathVariableResolver(resolver);
+    }
+    // </editor-fold>

Review Comment:
   Remove this // comment.
   



##########
src/main/java/org/apache/commons/xml/HardeningXPathFactory.java:
##########
@@ -0,0 +1,73 @@
+/*
+ * 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
+ *
+ *      https://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.commons.xml;
+
+import javax.xml.xpath.XPath;
+import javax.xml.xpath.XPathFactory;
+import javax.xml.xpath.XPathFactoryConfigurationException;
+import javax.xml.xpath.XPathFunctionResolver;
+import javax.xml.xpath.XPathVariableResolver;
+
+/**
+ * {@link XPathFactory} wrapper that returns a {@link HardeningXPath} from 
{@link #newXPath()}.
+ *
+ * <p>Required because {@link 
javax.xml.XMLConstants#FEATURE_SECURE_PROCESSING} on the factory governs only 
the XPath engine: the stock JDK and Apache Xalan
+ * implement the {@link org.xml.sax.InputSource}-taking {@code evaluate} entry 
points by provisioning an internal document parser the feature does not reach.
+ * The wrapper performs that document build itself through a hardened parser 
instead; see {@link HardeningXPath}.</p>
+ */
+final class HardeningXPathFactory extends XPathFactory {
+
+    private final XPathFactory delegate;
+
+    HardeningXPathFactory(final XPathFactory delegate) {
+        this.delegate = delegate;

Review Comment:
   Add Objects.requireNonNull().



##########
src/main/java/org/apache/commons/xml/HardeningXPathExpression.java:
##########
@@ -0,0 +1,62 @@
+/*
+ * 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
+ *
+ *      https://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.commons.xml;
+
+import javax.xml.namespace.QName;
+import javax.xml.xpath.XPathExpression;
+import javax.xml.xpath.XPathExpressionException;
+
+import org.xml.sax.InputSource;
+
+/**
+ * {@link XPathExpression} wrapper that applies the same {@link InputSource} 
rewrite as {@link HardeningXPath} to the compiled evaluation entry points.
+ *
+ * <p>{@link HardeningXPath#compile(String)} returns one of these, so {@link 
#evaluate(InputSource)} and {@link #evaluate(InputSource, QName)} build the
+ * document through a hardened, namespace-aware parser instead of the engine's 
own; the {@code evaluateExpression} default methods added by Java 9 route
+ * through these overloads as well.</p>
+ */
+final class HardeningXPathExpression implements XPathExpression {
+
+    private final XPathExpression delegate;
+
+    HardeningXPathExpression(final XPathExpression delegate) {
+        this.delegate = delegate;
+    }
+
+    @Override
+    public String evaluate(final InputSource source) throws 
XPathExpressionException {
+        return delegate.evaluate(HardeningXPath.parse(source));
+    }
+
+    @Override
+    public Object evaluate(final InputSource source, final QName returnType) 
throws XPathExpressionException {
+        return delegate.evaluate(HardeningXPath.parse(source), returnType);
+    }
+
+    // <editor-fold defaultstate="collapsed" desc="Trivial delegation">
+    @Override
+    public String evaluate(final Object item) throws XPathExpressionException {
+        return delegate.evaluate(item);
+    }
+
+    @Override
+    public Object evaluate(final Object item, final QName returnType) throws 
XPathExpressionException {
+        return delegate.evaluate(item, returnType);
+    }
+    // </editor-fold>

Review Comment:
   Remove this junk // comment.
   



##########
src/test/java/org/apache/commons/xml/ShadingFootprintTest.java:
##########
@@ -69,23 +70,25 @@ class ShadingFootprintTest {
     private static final Set<String> STAX_HARDENER = set("StaxHardener", 
"HardeningXMLInputFactory", "FallbackIgnoreXMLResolver", HARDENING_EXCEPTION);
 
     /**
-     * TrAX, XPath and schema re-harden their sub-parsers through {@link 
SAXParserHardener#harden(Source)}, so each builds on the full SAX closure below.
+     * TrAX, XPath and schema re-harden their sub-parsers through {@link 
SAXParserHardener#hardenSource(Source)}, so each builds on the full SAX closure 
below; XPath
+     * additionally parses InputSource-taking evaluate calls through the DOM 
hardener, so its closure carries that set too.
      */
     private static final Set<String> TRANSFORMER_HARDENER = 
saxParsersHardenerPlus("TransformerHardener", "HardeningTransformerFactory",
             "HardeningTransformer", "HardeningTransformerHandler", 
"HardeningTemplates", "HardeningTemplatesHandler", "HardeningXMLFilter",
             "FallbackIgnoreURIResolver", "SaxonProvider", "SaxonProvider$1", 
"SaxonProvider$HardenedConfiguration"
             , "SaxonProvider$SaxonProviderConfigurer");
 
-    private static final Set<String> XPATH_HARDENER = 
saxParsersHardenerPlus("XPathHardener", "FallbackIgnoreURIResolver", 
"SaxonProvider", "SaxonProvider$1",
-            "SaxonProvider$HardenedConfiguration", 
"SaxonProvider$SaxonProviderConfigurer");
+    private static final Set<String> XPATH_HARDENER = 
saxParsersHardenerPlus("XPathHardener", "FallbackIgnoreURIResolver", 
"SaxonProvider",
+            "SaxonProvider$1", "SaxonProvider$HardenedConfiguration", 
"SaxonProvider$SaxonProviderConfigurer", "HardeningXPathFactory", 
"HardeningXPath",
+            "HardeningXPathExpression", "DocumentBuilderHardener", 
"HardeningDocumentBuilder", "HardeningDocumentBuilderFactory");
 
     private static final Set<String> SCHEMA_HARDENER = 
saxParsersHardenerPlus("SchemaHardener", "HardeningSchemaFactory", 
"HardeningValidator",
             "HardeningValidatorHandler", "HardeningSchema", 
"FallbackIgnoreLSResourceResolver");
 
     /**
      * Only the public {@link XmlFactories} entry, which news up every 
hardener, still pulls the whole library; this is its class count.
      */
-    private static final int WHOLE_LIBRARY_SIZE = 32;
+    private static final int WHOLE_LIBRARY_SIZE = 35;

Review Comment:
   The Javadoc says "class count" but the name uses the ambiguous "SIZE", use 
"CLASS_COUNT" to remove the confusion.
   



##########
src/main/java/org/apache/commons/xml/HardeningXPath.java:
##########
@@ -0,0 +1,141 @@
+/*
+ * 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
+ *
+ *      https://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.commons.xml;
+
+import java.io.IOException;
+
+import javax.xml.namespace.NamespaceContext;
+import javax.xml.namespace.QName;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.xpath.XPath;
+import javax.xml.xpath.XPathExpression;
+import javax.xml.xpath.XPathExpressionException;
+import javax.xml.xpath.XPathFunctionResolver;
+import javax.xml.xpath.XPathVariableResolver;
+
+import org.w3c.dom.Document;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+
+/**
+ * {@link XPath} wrapper that performs the document build behind every {@link 
InputSource}-taking {@code evaluate} call with a hardened, namespace-aware
+ * {@link javax.xml.parsers.DocumentBuilder} and evaluates the delegate 
against the parsed {@link Document}, so the engine's own parser never runs.
+ *
+ * <p>The JAXP contract for {@link XPath#evaluate(String, InputSource, QName)} 
is "build a document from the source, then evaluate against it", and both the
+ * stock JDK and Apache Xalan provision an internal parser for that build 
which {@link javax.xml.XMLConstants#FEATURE_SECURE_PROCESSING} on the
+ * {@link javax.xml.xpath.XPathFactory} does not reach. Parsing here puts the 
build on the library's resolver floor: an external reference inside the document
+ * resolves to empty content, so it is neither fetched nor leaked, and the 
evaluation proceeds on whatever the parse produced. {@link #compile(String)} 
wraps
+ * the compiled expression in a {@link HardeningXPathExpression} on the same 
terms.</p>
+ *
+ * <p>The {@code evaluateExpression} default methods added to the interface by 
Java 9 route through the {@code evaluate} overloads overridden here, so they
+ * carry the same rewrite on newer runtimes even though this class targets 
Java 8.</p>
+ */
+final class HardeningXPath implements XPath {
+
+    /**
+     * Parses the source through a hardened, namespace-aware {@link 
javax.xml.parsers.DocumentBuilder}, mirroring the namespace awareness of the 
parser the
+     * engine would have provisioned.
+     *
+     * @param source The document to evaluate against.
+     * @return The parsed document.
+     * @throws NullPointerException     if {@code source} is {@code null}, per 
the {@link XPath} contract.
+     * @throws XPathExpressionException if the source cannot be parsed.
+     */
+    static Document parse(final InputSource source) throws 
XPathExpressionException {
+        if (source == null) {
+            throw new NullPointerException("source cannot be null");
+        }
+        try {
+            final DocumentBuilderFactory factory = 
DocumentBuilderHardener.harden(DocumentBuilderFactory.newInstance());
+            factory.setNamespaceAware(true);
+            return factory.newDocumentBuilder().parse(source);
+        } catch (final ParserConfigurationException | SAXException | 
IOException e) {
+            throw new XPathExpressionException(e);
+        }
+    }
+
+    private final XPath delegate;
+
+    HardeningXPath(final XPath delegate) {
+        this.delegate = delegate;

Review Comment:
   Make invariant obvious:
   ```
           this.delegate = Objects.requireNonNull(delegate, "delegate");
   ```



##########
src/test/java/org/apache/commons/xml/XPathInputSourceTest.java:
##########
@@ -0,0 +1,88 @@
+/*
+ * 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
+ *
+ *      https://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.commons.xml;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import javax.xml.xpath.XPathExpressionException;
+import javax.xml.xpath.XPathFactory;
+
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Checks that the document parse behind {@code XPath.evaluate(String, 
InputSource)} (and its compiled {@code XPathExpression} counterpart) cannot 
pull in an
+ * external general entity.
+ *
+ * <p>The stock JDK and Apache Xalan implement the {@link 
org.xml.sax.InputSource}-taking {@code evaluate} entry points by provisioning 
an internal document
+ * parser that {@code FEATURE_SECURE_PROCESSING} on the {@link XPathFactory} 
does not reach. The {@link HardeningXPathFactory} wrapper parses the input
+ * through a hardened {@code DocumentBuilder} instead, so the external 
reference resolves to empty on the floor (or the parse is rejected outright), 
while the
+ * evaluation itself still works. Tagged {@code xpath}, so it runs under 
test-stockjdk, test-xalan and test-xalan-xerces; the Saxon engine takes the 
separate
+ * {@code SaxonProvider} path covered by {@code 
SaxonXPathExternalCallsTest}.</p>
+ */
+@Tag("xpath")
+class XPathInputSourceTest {
+
+    private static final String EXPRESSION = "string(/root/child)";
+
+    /** {@link AttackTestSupport#xmlBody} content whose single entity 
reference resolves to {@link AttackTestSupport#LEAKED_MARKER} if the DTD is 
fetched. */
+    private static String entityPayload() {
+        return "<?xml version=\"1.0\"?>\n"
+                + "<!DOCTYPE root [\n  <!ENTITY xxe SYSTEM \"" + 
AttackTestSupport.resourceUrl("referenced.txt") + "\">\n]>\n"
+                + AttackTestSupport.xmlBody("&xxe;");
+    }
+
+    @Test
+    void hardenedXPathEvaluateDoesNotLeak() throws Exception {
+        final String result;
+        try {
+            result = 
XmlFactories.newXPathFactory().newXPath().evaluate(EXPRESSION, 
AttackTestSupport.inputSource(entityPayload()));
+        } catch (final XPathExpressionException blocked) {
+            return; // Acceptable: the parse rejected the reference rather 
than resolving it to empty.
+        }
+        assertFalse(result.contains(AttackTestSupport.LEAKED_MARKER), 
"external entity leaked into the XPath result: " + result);
+    }
+
+    @Test
+    void hardenedXPathExpressionEvaluateDoesNotLeak() throws Exception {
+        final String result;
+        try {
+            result = 
XmlFactories.newXPathFactory().newXPath().compile(EXPRESSION).evaluate(AttackTestSupport.inputSource(entityPayload()));
+        } catch (final XPathExpressionException blocked) {

Review Comment:
   Same comment as in PR #42 
   Add a better comment or use `assertThrows()`



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to