From 1b18bdcd7871aa2c1b1bb5a376085abc165b2392 Mon Sep 17 00:00:00 2001 From: mohammed arib Date: Fri, 3 Jul 2026 13:51:00 +0530 Subject: [PATCH] disable external entity resolution in text_xml writeTo Signed-off-by: mohammed arib --- .../eclipse/angus/mail/handlers/text_xml.java | 23 ++++++++++++-- .../angus/mail/handlers/TextXmlTest.java | 30 +++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/org/eclipse/angus/mail/handlers/text_xml.java b/core/src/main/java/org/eclipse/angus/mail/handlers/text_xml.java index a9ae2172..c55b557a 100644 --- a/core/src/main/java/org/eclipse/angus/mail/handlers/text_xml.java +++ b/core/src/main/java/org/eclipse/angus/mail/handlers/text_xml.java @@ -21,8 +21,10 @@ import jakarta.mail.internet.ContentType; import jakarta.mail.internet.ParseException; +import javax.xml.XMLConstants; import javax.xml.transform.Source; import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerConfigurationException; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.stream.StreamResult; @@ -86,8 +88,16 @@ public void writeTo(Object obj, String mimeType, OutputStream os) } try { - Transformer transformer = - TransformerFactory.newInstance().newTransformer(); + TransformerFactory tf = TransformerFactory.newInstance(); + // a DataSource/Source can carry attacker controlled XML, so don't + // let the identity transform resolve external entities or DTDs + try { + tf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); + } catch (TransformerConfigurationException ignored) { + } + setAttribute(tf, XMLConstants.ACCESS_EXTERNAL_DTD, ""); + setAttribute(tf, XMLConstants.ACCESS_EXTERNAL_STYLESHEET, ""); + Transformer transformer = tf.newTransformer(); StreamResult result = new StreamResult(os); if (obj instanceof DataSource) { // Streaming transform applies only to @@ -107,6 +117,15 @@ public void writeTo(Object obj, String mimeType, OutputStream os) } } + private static void setAttribute(TransformerFactory tf, String name, + Object value) { + try { + tf.setAttribute(name, value); + } catch (IllegalArgumentException ignored) { + // attribute not supported by this implementation + } + } + private boolean isXmlType(String type) { try { ContentType ct = new ContentType(type); diff --git a/providers/angus-mail/src/test/java/org/eclipse/angus/mail/handlers/TextXmlTest.java b/providers/angus-mail/src/test/java/org/eclipse/angus/mail/handlers/TextXmlTest.java index a567daf5..9bb0e8d2 100644 --- a/providers/angus-mail/src/test/java/org/eclipse/angus/mail/handlers/TextXmlTest.java +++ b/providers/angus-mail/src/test/java/org/eclipse/angus/mail/handlers/TextXmlTest.java @@ -25,11 +25,15 @@ import javax.xml.transform.stream.StreamSource; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; +import java.io.Writer; import java.nio.charset.StandardCharsets; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; /** @@ -104,6 +108,32 @@ public void testSourceToStream() throws Exception { assertTrue(sc.contains(xml.trim())); } + // an external entity in the source must not be resolved into the output + @Test + public void testExternalEntityNotResolved() throws Exception { + File secret = File.createTempFile("textxml", ".txt"); + secret.deleteOnExit(); + try (Writer w = new FileWriter(secret)) { + w.write("TOPSECRET"); + } + String doc = "\n" + + " ]>\n&x;"; + DataContentHandler dch = new text_xml(); + DataSource ds = new ByteArrayDataSource( + doc.getBytes(StandardCharsets.US_ASCII), "text/xml"); + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + try { + dch.writeTo(ds, "text/xml", bos); + } catch (IOException expected) { + // entity resolution disabled, transform refuses the document + return; + } + String out = new String(bos.toByteArray(), StandardCharsets.US_ASCII); + assertFalse("external entity was resolved: " + out, + out.contains("TOPSECRET")); + } + /** * Read a stream into a String. */