This is an automated email from the ASF dual-hosted git repository.
garydgregory pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/commons-secure-xml.git
The following commit(s) were added to refs/heads/main by this push:
new da38c00 Create the Transformer of an XMLFilter eagerly and reuse it
(#95)
da38c00 is described below
commit da38c00b2499d0e6b61985ba5e8f7b8aea315ce8
Author: Piotr P. Karwasz <[email protected]>
AuthorDate: Mon Sep 14 00:42:42 2026 +0200
Create the Transformer of an XMLFilter eagerly and reuse it (#95)
* Create the Transformer of an XMLFilter eagerly.
SAXTransformerFactory.newXMLFilter now builds the Transformer the filter
drives, so a failure to create it is reported from newXMLFilter rather than
from the first parse, and every parse of a filter runs on that one
Transformer, the way the stock TrAX filters do.
The work a parse used to repeat moves to the setters: setParent wires the
resolver, DTD and error callbacks onto the parent, and setContentHandler
builds the SAXResult the transformation writes to. It is the filter that is
installed on the parent, not the caller's callbacks, so a resolver or
handler the caller sets afterwards is still reached.
The ContentHandler role inherited from XMLFilterImpl is not usable: events
pushed into it would reach the caller's handler untransformed, so
startDocument now fails and names the TrAX shape for that job.
Assisted-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01AaDyR9HjZLWkFn42x7kje3
* Report a missing XMLFilter in the TrAX shape.
newXMLFilter(Source) handed back the null its implementation returned for a
stylesheet that failed to compile, while newXMLFilter(Templates) threw when
the Templates produced no Transformer. Both now report the failure as a
TransformerConfigurationException: a filter has no null to hand back, and
null in that contract means the factory supports no filters at all.
The products the factory wraps rather than synthesizes keep preserving the
implementation's null.
Assisted-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01AaDyR9HjZLWkFn42x7kje3
* Document the type parameter of the null check.
Checkstyle 14.1.0, which the build resolves on recent JDKs, requires a
@param tag for a method's type parameter.
Assisted-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01AaDyR9HjZLWkFn42x7kje3
* fix: remove implementation details from doc
---
src/changes/changes.xml | 2 +
.../commons/xml/secure/SecureTransformer.java | 2 +-
.../xml/secure/SecureTransformerFactory.java | 26 +++++-
.../apache/commons/xml/secure/SecureXMLFilter.java | 100 ++++++++++++++++-----
.../xml/secure/SecureTransformerFactoryTest.java | 23 ++++-
.../commons/xml/secure/SecureXMLFilterTest.java | 34 ++++++-
.../apache/commons/xml/secure/XMLFilterTest.java | 22 ++++-
7 files changed, 173 insertions(+), 36 deletions(-)
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 3c8af33..837d9cd 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -37,6 +37,8 @@ The <action> type attribute can be add, update, fix, or
remove.
<action type="fix" dev="ggregory" due-to="Gary Gregory">Fix the
OpenRewrite migration recipe to add a dependency on
org.apache.commons:commons-secure-xml:1.0.0.</action>
<action type="fix" dev="pkarwasz" due-to="Piotr P. Karwasz, Gary
Gregory">Fix rejection behavior of foreign Templates in
SAXTransformerFactory.newTransformerHandler.</action>
<action type="fix" dev="pkarwasz" due-to="Piotr P. Karwasz, Gary
Gregory">Fix a NullPointerException when an XMLFilter with a self-driven parent
reader is parsed with a null InputSource. #86</action>
+ <action type="fix" dev="pkarwasz" due-to="Piotr P. Karwasz, Gary
Gregory">Create the Transformer of an XMLFilter eagerly and reuse it for every
parse.</action>
+ <action type="fix" dev="pkarwasz" due-to="Piotr P. Karwasz, Gary
Gregory">Report a stylesheet that produces no XMLFilter as a
TransformerConfigurationException instead of returning null.</action>
<!-- ADD -->
<!-- UPDATE -->
<action dev="ggregory" type="update" due-to="Gary Gregory">Bump
org.apache.commons:commons-parent from 104 to 105.</action>
diff --git a/src/main/java/org/apache/commons/xml/secure/SecureTransformer.java
b/src/main/java/org/apache/commons/xml/secure/SecureTransformer.java
index 1ec40d5..d84127c 100644
--- a/src/main/java/org/apache/commons/xml/secure/SecureTransformer.java
+++ b/src/main/java/org/apache/commons/xml/secure/SecureTransformer.java
@@ -54,7 +54,7 @@ final class SecureTransformer extends Transformer {
* Snapshot of the factory's {@value
SecureSAXParserFactory#OVERRIDE_DEFAULT_PARSER} outcome at creation, like the
JDK copies the feature onto the
* transformers it creates.
*/
- private final boolean overrideDefaultParser;
+ final boolean overrideDefaultParser;
/**
* Constructs a new instance.
diff --git
a/src/main/java/org/apache/commons/xml/secure/SecureTransformerFactory.java
b/src/main/java/org/apache/commons/xml/secure/SecureTransformerFactory.java
index a609e1e..8b6bd48 100644
--- a/src/main/java/org/apache/commons/xml/secure/SecureTransformerFactory.java
+++ b/src/main/java/org/apache/commons/xml/secure/SecureTransformerFactory.java
@@ -148,6 +148,23 @@ private static boolean probeOverrideDefaultParser(final
SAXTransformerFactory fa
}
}
+ /**
+ * Returns the value, or reports its absence in the TrAX shape.
+ *
+ * @param <T> The type of the product.
+ * @param value The value an implementation produced.
+ * @param what Name of the missing product, for the message.
+ * @return The value, never {@code null}.
+ * @throws TransformerConfigurationException Thrown if {@code value}
is {@code null}.
+ */
+ private static <T> T required(final T value, final String what) throws
TransformerConfigurationException {
+ // Xalan hands back null instead of throwing when the stylesheet
failed to compile: XALANJ-2410.
+ if (value == null) {
+ throw new TransformerConfigurationException("Underlying
implementation returned a null " + what + ".");
+ }
+ return value;
+ }
+
private static Templates unwrap(final Templates templates) {
return templates instanceof SecureTemplates ? ((SecureTemplates)
templates).getDelegate() : templates;
}
@@ -336,14 +353,15 @@ public TransformerHandler newTransformerHandler(final
Templates templates) throw
*/
@Override
public XMLFilter newXMLFilter(final Source source) throws
TransformerConfigurationException {
- final Templates templates = newTemplates(source);
- return templates == null ? null : new
SecureXMLFilter((SecureTemplates) templates);
+ return newXMLFilter(required(newTemplates(source), "Templates"));
}
@Override
public XMLFilter newXMLFilter(final Templates templates) throws
TransformerConfigurationException {
- return new SecureXMLFilter(templates instanceof SecureTemplates ?
(SecureTemplates) templates
- : new SecureTemplates(templates, getURIResolver(),
emptySource, overrideDefaultParser()));
+ final Transformer transformer =
required(templates.newTransformer(), "Transformer");
+ return new SecureXMLFilter(transformer instanceof SecureTransformer
+ ? (SecureTransformer) transformer
+ : new SecureTransformer(transformer, getURIResolver(),
emptySource, overrideDefaultParser()));
}
/**
diff --git a/src/main/java/org/apache/commons/xml/secure/SecureXMLFilter.java
b/src/main/java/org/apache/commons/xml/secure/SecureXMLFilter.java
index 1287c65..b887c30 100644
--- a/src/main/java/org/apache/commons/xml/secure/SecureXMLFilter.java
+++ b/src/main/java/org/apache/commons/xml/secure/SecureXMLFilter.java
@@ -38,7 +38,7 @@
import org.xml.sax.helpers.XMLFilterImpl;
/**
- * {@link XMLFilter} that transforms the parsed input through a {@link
SecureTemplates} and emits the result as SAX events.
+ * {@link XMLFilter} that transforms the parsed input through a {@link
SecureTransformer} and emits the result as SAX events.
*
* <p>Composed from the library's own wrappers instead of delegating to the
implementation's filter, because the implementation filters self-provision an
* unsecured reader for the input (the stock JDK's does so as early as {@code
setContentHandler}) and cast a supplied {@link javax.xml.transform.Templates} to
@@ -46,6 +46,10 @@
* caller has not set a parent (a caller-set parent is trusted configuration,
used as-is), and the transformation runs on a {@link SecureTransformer}, so
* runtime {@code document()} sits on the resolver floor. The filter is also
the transformer's {@link ErrorListener}, forwarding TrAX error reports to the
* caller-set {@link org.xml.sax.ErrorHandler} the way the parent reader's SAX
reports are.</p>
+ *
+ * <p>Every parse runs on the one {@link SecureTransformer} the filter is
constructed with, the way every stock TrAX filter is built from a single
+ * Transformer. The filter is therefore reusable for successive parses and
inherits that transformer's reuse contract: one parse at a time, not two
threads at
+ * once.</p>
*/
final class SecureXMLFilter extends XMLFilterImpl implements ErrorListener {
@@ -71,16 +75,25 @@ private static SAXParseException toSAXParseException(final
TransformerException
: new SAXParseException(e.getMessage(), locator.getPublicId(),
locator.getSystemId(), locator.getLineNumber(), locator.getColumnNumber(),
embedded);
}
- private final SecureTemplates templates;
+ /** Snapshot of the transformer's {@value
SecureSAXParserFactory#OVERRIDE_DEFAULT_PARSER} outcome, carried onto the
self-provisioned parent reader. */
+ private final boolean overrideDefaultParser;
+
+ /** Where the transformation writes, rebuilt whenever the caller sets a
ContentHandler; {@code null} until one is set. */
+ private SAXResult result;
+
+ private final Transformer transformer;
/**
* Constructs a new instance.
*
- * @param templates The templates to wrap; must not be {@code null}.
- * @throws NullPointerException Thrown if {@code templates} is {@code
null}.
+ * @param transformer The transformer every parse runs on; must not be
{@code null}.
+ * @throws NullPointerException Thrown if {@code transformer} is {@code
null}.
*/
- SecureXMLFilter(final SecureTemplates templates) {
- this.templates = Objects.requireNonNull(templates, "templates");
+ SecureXMLFilter(final SecureTransformer transformer) {
+ this.transformer = Objects.requireNonNull(transformer, "transformer");
+ this.overrideDefaultParser = transformer.overrideDefaultParser;
+ // The filter is the listener, so TrAX error reports reach the
caller-set ErrorHandler like the parent reader's SAX reports do.
+ transformer.setErrorListener(this);
}
/**
@@ -110,6 +123,15 @@ public void fatalError(final TransformerException e)
throws TransformerException
throw e;
}
+ /**
+ * Gets the Transformer this filter drives.
+ *
+ * @return the filter's {@link Transformer}, never {@code null}.
+ */
+ Transformer getTransformer() {
+ return transformer;
+ }
+
/**
* {@inheritDoc}
*
@@ -118,31 +140,16 @@ public void fatalError(final TransformerException e)
throws TransformerException
*/
@Override
public void parse(final InputSource input) throws SAXException,
IOException {
- final ContentHandler handler = getContentHandler();
- if (handler == null) {
+ if (result == null) {
throw new SAXException("No ContentHandler set on the XMLFilter to
receive the transformation result");
}
if (getParent() == null) {
-
setParent(SecureSAXParserFactory.newXMLReader(templates.overrideDefaultParser));
- }
- final XMLReader parent = getParent();
- // Like XMLFilterImpl.setupParse, minus the ContentHandler: the
transformer owns the parent's content events and delivers the transformed
stream to
- // the caller's handler through the SAXResult instead.
- parent.setEntityResolver(this);
- parent.setDTDHandler(this);
- parent.setErrorHandler(this);
- final SAXResult result = new SAXResult(handler);
- if (handler instanceof LexicalHandler) {
- result.setLexicalHandler((LexicalHandler) handler);
+
setParent(SecureSAXParserFactory.newXMLReader(overrideDefaultParser));
}
try {
- // A new SecureTransformer per parse: the floor is installed on
it, and transformers are not reusable across concurrent parses.
- final Transformer transformer = templates.newTransformer();
- // The filter is the listener, so TrAX error reports reach the
caller-set ErrorHandler like the parent reader's SAX reports do.
- transformer.setErrorListener(this);
// A self-driven parent needs no InputSource, so a caller may pass
null here; most TrAX implementations dereference the one they get unchecked.
// See: https://issues.apache.org/jira/browse/XALANJ-2851
- transformer.transform(new SAXSource(parent, input != null ? input
: new InputSource(NO_INPUT_SYSTEM_ID)), result);
+ transformer.transform(new SAXSource(getParent(), input != null ?
input : new InputSource(NO_INPUT_SYSTEM_ID)), result);
} catch (final TransformerException e) {
// The parent reader's parse errors and the handler's own
exceptions arrive wrapped; rethrow the original rather than nesting the
hierarchies.
final Throwable cause = e.getCause();
@@ -156,6 +163,51 @@ public void parse(final InputSource input) throws
SAXException, IOException {
}
}
+ /**
+ * {@inheritDoc}
+ *
+ * <p>Builds the destination the transformation writes to, so a parse only
has to run it. A handler that is also a {@link LexicalHandler} receives the
+ * result's comments and CDATA boundaries too, the way {@link
javax.xml.transform.sax.SAXResult} expects them to be supplied.</p>
+ */
+ @Override
+ public void setContentHandler(final ContentHandler handler) {
+ super.setContentHandler(handler);
+ result = handler == null ? null : new SAXResult(handler);
+ if (handler instanceof LexicalHandler) {
+ result.setLexicalHandler((LexicalHandler) handler);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * <p>Wires the filter onto the new parent the way {@link
XMLFilterImpl#setupParse()} would, minus the ContentHandler: the transformer
owns the parent's
+ * content events and delivers the transformed stream to the caller's
handler through a {@link SAXResult} instead. Wiring the parent here rather than
per
+ * parse is enough because it is the filter that is installed, not the
caller's callbacks, so a callback the caller sets afterwards is still
reached.</p>
+ */
+ @Override
+ public void setParent(final XMLReader parent) {
+ super.setParent(parent);
+ // XMLFilterImpl tolerates a null parent, so do not wire one.
+ if (parent != null) {
+ parent.setEntityResolver(this);
+ parent.setDTDHandler(this);
+ parent.setErrorHandler(this);
+ }
+ }
+
+ /**
+ * Fails: events pushed into the {@link ContentHandler} role inherited
from {@link XMLFilterImpl} would reach the caller's handler untransformed.
+ *
+ * <p>The stock filters make that role inert too, by dropping the events
(Apache Xalan, the JDK) or by not implementing it at all (Saxon).</p>
+ *
+ * @throws SAXException Always.
+ */
+ @Override
+ public void startDocument() throws SAXException {
+ throw new SAXException("This XMLFilter only implements ContentHandler
for technical reasons. To push SAX events, use newTransformerHandler instead.");
+ }
+
/** Forwards a transformation warning to the caller-set {@link
org.xml.sax.ErrorHandler}; the transformation continues unless that handler
throws. */
@Override
public void warning(final TransformerException e) throws
TransformerException {
diff --git
a/src/test/java/org/apache/commons/xml/secure/SecureTransformerFactoryTest.java
b/src/test/java/org/apache/commons/xml/secure/SecureTransformerFactoryTest.java
index 8f83029..6ca4491 100644
---
a/src/test/java/org/apache/commons/xml/secure/SecureTransformerFactoryTest.java
+++
b/src/test/java/org/apache/commons/xml/secure/SecureTransformerFactoryTest.java
@@ -186,7 +186,8 @@ void preservesNullResultsFromEveryWrappableProduct() throws
Exception {
assertNull(factory.newTransformerHandler());
assertNull(factory.newTransformerHandler(stylesheet()));
assertNull(factory.newTransformerHandler(templates));
- assertNull(factory.newXMLFilter(stylesheet()));
+ // A filter has no null to hand back: a null in this contract would
mean the factory has no filters at all.
+ assertThrows(TransformerConfigurationException.class, () ->
factory.newXMLFilter(stylesheet()));
factory.setAttribute("test", "value");
assertEquals("value", factory.getAttribute("test"));
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
@@ -234,6 +235,26 @@ public Transformer newTransformer() throws
TransformerConfigurationException {
}
}
+ @Test
+ void rejectsTemplatesThatProduceNoTransformer() {
+ final Templates templates = new Templates() {
+
+ @Override
+ public Properties getOutputProperties() {
+ return new Properties();
+ }
+
+ @Override
+ public Transformer newTransformer() {
+ // Xalan hands back null instead of throwing when the
stylesheet failed to compile: XALANJ-2410.
+ return null;
+ }
+ };
+ final SAXTransformerFactory factory = (SAXTransformerFactory)
SecureTransformerFactory.newInstance();
+ // A filter has no null to hand back, so the null the wrappers
preserve from Templates is reported in the TrAX shape instead.
+ assertThrows(TransformerConfigurationException.class, () ->
factory.newXMLFilter(templates));
+ }
+
@Test
void securesAssociatedStylesheetSourcesOfEverySupportedShape() throws
Exception {
final SAXTransformerFactory factory = (SAXTransformerFactory)
SecureTransformerFactory.newInstance();
diff --git
a/src/test/java/org/apache/commons/xml/secure/SecureXMLFilterTest.java
b/src/test/java/org/apache/commons/xml/secure/SecureXMLFilterTest.java
index 5f68d0b..a17ff84 100644
--- a/src/test/java/org/apache/commons/xml/secure/SecureXMLFilterTest.java
+++ b/src/test/java/org/apache/commons/xml/secure/SecureXMLFilterTest.java
@@ -18,6 +18,7 @@
package org.apache.commons.xml.secure;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -53,7 +54,7 @@ class SecureXMLFilterTest {
private static SecureXMLFilter filter() throws Exception {
final Templates templates =
TransformerFactory.newInstance().newTemplates(new StreamSource(new StringReader(
"<xsl:stylesheet version='1.0'
xmlns:xsl='http://www.w3.org/1999/XSL/Transform'><xsl:template
match='@*|node()'><xsl:copy><xsl:apply-templates
select='@*|node()'/></xsl:copy></xsl:template></xsl:stylesheet>")));
- return new SecureXMLFilter(new SecureTemplates(templates, null, null,
false));
+ return new SecureXMLFilter(new
SecureTransformer(templates.newTransformer(), null, null, false));
}
@Test
@@ -71,6 +72,14 @@ public void startElement(final String uri, final String
localName, final String
assertEquals("handler", exception.getMessage());
}
+ @Test
+ void rejectsSaxEventsPushedIntoTheFilter() throws Exception {
+ final SecureXMLFilter filter = filter();
+ filter.setContentHandler(new DefaultHandler());
+ // The ContentHandler role is inherited from XMLFilterImpl; events
pushed in that way would reach the handler untransformed.
+ assertThrows(SAXException.class, filter::startDocument);
+ }
+
@Test
void reportsWarningErrorAndFatalErrorUsingSaxShape() throws Exception {
final SecureXMLFilter filter = filter();
@@ -188,12 +197,33 @@ public void transform(final Source source, final Result
result) throws Transform
};
}
};
- final SecureXMLFilter filter = new SecureXMLFilter(new
SecureTemplates(templates, null, null, false));
+ final SecureXMLFilter filter = new SecureXMLFilter(new
SecureTransformer(templates.newTransformer(), null, null, false));
filter.setContentHandler(new DefaultHandler());
final IOException exception = assertThrows(IOException.class, () ->
filter.parse(new InputSource(new StringReader("<root/>"))));
assertEquals("transform", exception.getMessage());
}
+ @Test
+ void reusesOneTransformerAcrossParses() throws Exception {
+ final Templates templates =
TransformerFactory.newInstance().newTemplates(new StreamSource(new StringReader(
+ "<xsl:stylesheet version='1.0'
xmlns:xsl='http://www.w3.org/1999/XSL/Transform'><xsl:param name='p'/>"
+ + "<xsl:template match='/'><out><xsl:value-of
select='$p'/></out></xsl:template></xsl:stylesheet>")));
+ final SecureXMLFilter filter = new SecureXMLFilter(new
SecureTransformer(templates.newTransformer(), null, null, false));
+ final Transformer transformer = filter.getTransformer();
+ assertInstanceOf(SecureTransformer.class, transformer, "the reused
Transformer must carry the resolver floor");
+ transformer.setParameter("p", "carried");
+ final StringBuilder first = new StringBuilder();
+ filter.setContentHandler(AttackTestSupport.capturingHandler(first));
+ filter.parse(new InputSource(new StringReader("<root/>")));
+ assertEquals("carried", first.toString());
+ final StringBuilder second = new StringBuilder();
+ filter.setContentHandler(AttackTestSupport.capturingHandler(second));
+ filter.parse(new InputSource(new StringReader("<root/>")));
+ // The parameter survives the first parse only because the second runs
on the same Transformer.
+ assertEquals("carried", second.toString());
+ assertSame(transformer, filter.getTransformer());
+ }
+
@Test
void sendsLexicalEventsToALexicalContentHandler() throws Exception {
final SecureXMLFilter filter = filter();
diff --git a/src/test/java/org/apache/commons/xml/secure/XMLFilterTest.java
b/src/test/java/org/apache/commons/xml/secure/XMLFilterTest.java
index d57e355..e1cedd3 100644
--- a/src/test/java/org/apache/commons/xml/secure/XMLFilterTest.java
+++ b/src/test/java/org/apache/commons/xml/secure/XMLFilterTest.java
@@ -168,6 +168,14 @@ void secureFilterDoesNotLeakDocument() throws Exception {
assertFalse(filterAndCapture(filter,
"<root/>").contains(AttackTestSupport.LEAKED_MARKER), "document() through
XMLFilter leaked");
}
+ @Test
+ void secureFilterDoesNotLeakDocumentOnRepeatedParse() throws Exception {
+ // One Transformer drives every parse of a filter, so the floor has to
hold on the second parse as it did on the first.
+ final XMLFilter filter =
SaxSurfaceTestSupport.secureFactory().newXMLFilter(AttackTestSupport.resourceSource("with-document.xsl"));
+ assertFalse(filterAndCapture(filter,
"<root/>").contains(AttackTestSupport.LEAKED_MARKER), "document() leaked on the
first parse");
+ assertFalse(filterAndCapture(filter,
"<root/>").contains(AttackTestSupport.LEAKED_MARKER), "document() leaked on the
second parse");
+ }
+
@Test
void secureFilterDoesNotLeakExternalEntity() throws Exception {
// The f003 vector: with no caller-set parent, the input must be
parsed by a secure reader, not a self-provisioned permissive one.
@@ -182,6 +190,8 @@ void secureFilterDoesNotReWrapParseError() throws Exception
{
filter.setErrorHandler(AttackTestSupport.STRICT_REPORTER);
final SAXException e = assertThrows(SAXException.class, () ->
filter.parse(new InputSource(new StringReader("<root>"))));
assertNotReWrapped(e);
+ // The parent is wired once, when it is set, so a repeated parse must
still report to the caller's ErrorHandler.
+ assertNotReWrapped(assertThrows(SAXException.class, () ->
filter.parse(new InputSource(new StringReader("<root>")))));
}
@Test
@@ -230,25 +240,29 @@ public void startDocument() throws SAXException {
@Test
void secureFilterRoutesEntityResolverToParent() throws Exception {
- // parse must wire the caller-set EntityResolver to the parent reader,
chaining it onto the floor so a caller can opt a specific entity in.
+ // setParent must wire the caller-set EntityResolver to the parent
reader, chaining it onto the floor so a caller can opt a specific entity in.
Assumptions.assumeFalse(AttackTestSupport.IS_ANDROID, "Android's Expat
does not resolve the external general entity here");
final XMLFilter filter =
SaxSurfaceTestSupport.secureFactory().newXMLFilter(AttackTestSupport.streamSource(IDENTITY_XSLT));
filter.setEntityResolver((publicId, systemId) -> new InputSource(new
StringReader("resolved-by-caller")));
final String output = filterAndCapture(filter, entityPayload());
assertTrue(output.contains("resolved-by-caller"), "caller-set
EntityResolver should opt the external entity in through the parent");
assertFalse(output.contains(AttackTestSupport.LEAKED_MARKER), "the
real external resource must not be fetched");
+ // The parent is wired once, when it is set, so a repeated parse must
still reach the caller's resolver.
+ final String repeated = filterAndCapture(filter, entityPayload());
+ assertTrue(repeated.contains("resolved-by-caller"), "caller-set
EntityResolver should still be reached on a repeated parse");
+ assertFalse(repeated.contains(AttackTestSupport.LEAKED_MARKER), "the
real external resource must not be fetched on a repeated parse");
}
@Test
void secureFilterWiresCallbacksToParent() throws Exception {
- // parse must perform the XMLFilterImpl.setupParse wiring on the
parent for the resolver, DTD and error callbacks (the ContentHandler is owned
by the
- // transformer). The wiring calls are asserted directly on a recording
parent: which of them the implementation later consults or overwrites varies.
+ // setParent must perform the XMLFilterImpl.setupParse wiring on the
parent for the resolver, DTD and error callbacks (the ContentHandler is owned by
+ // the transformer). The wiring calls are asserted directly on a
recording parent: which of them the implementation later consults or overwrites
varies.
final XMLFilter filter =
SaxSurfaceTestSupport.secureFactory().newXMLFilter(AttackTestSupport.streamSource(IDENTITY_XSLT));
final SelfDrivenParent parent = new SelfDrivenParent();
filter.setParent(parent);
assertEquals("", filterAndCapture(filter, "<ignored/>"));
assertEquals(3, parent.wired.stream().filter(callback -> callback ==
filter).count(),
- "parse should wire the filter as the parent's EntityResolver,
DTDHandler and ErrorHandler: " + parent.wired);
+ "setParent should wire the filter as the parent's
EntityResolver, DTDHandler and ErrorHandler: " + parent.wired);
}
@Test