Skip to content

Commit 77081dc

Browse files
authored
Merge commit from fork
1 parent b68fc24 commit 77081dc

File tree

3 files changed

+174
-104
lines changed

3 files changed

+174
-104
lines changed

assertj-core/src/main/java/org/assertj/core/util/xml/XmlStringPrettyFormatter.java

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,13 @@
2020
import java.io.StringReader;
2121
import java.io.StringWriter;
2222
import java.io.Writer;
23+
import java.util.HashMap;
24+
import java.util.Map;
2325

26+
import javax.xml.XMLConstants;
2427
import javax.xml.parsers.DocumentBuilder;
2528
import javax.xml.parsers.DocumentBuilderFactory;
29+
import javax.xml.parsers.ParserConfigurationException;
2630

2731
import org.w3c.dom.Document;
2832
import org.w3c.dom.bootstrap.DOMImplementationRegistry;
@@ -42,6 +46,17 @@ public class XmlStringPrettyFormatter {
4246

4347
private static final String FORMAT_ERROR = "Unable to format XML string";
4448

49+
// https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html
50+
private static final Map<String, Boolean> SECURE_FEATURES = new HashMap<>();
51+
52+
static {
53+
SECURE_FEATURES.put(XMLConstants.FEATURE_SECURE_PROCESSING, true);
54+
SECURE_FEATURES.put("http://apache.org/xml/features/disallow-doctype-decl", true);
55+
SECURE_FEATURES.put("http://xml.org/sax/features/external-general-entities", false);
56+
SECURE_FEATURES.put("http://xml.org/sax/features/external-parameter-entities", false);
57+
SECURE_FEATURES.put("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
58+
}
59+
4560
public static String xmlPrettyFormat(String xmlStringToFormat) {
4661
checkArgument(xmlStringToFormat != null, "Expecting XML String not to be null");
4762
// convert String to an XML Document and then back to String but prettily formatted.
@@ -70,13 +85,23 @@ private static String prettyFormat(Document document, boolean keepXmlDeclaration
7085
private static Document toXmlDocument(String xmlString) {
7186
try {
7287
InputSource xmlInputSource = new InputSource(new StringReader(xmlString));
73-
DocumentBuilder xmlDocumentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
88+
DocumentBuilder xmlDocumentBuilder = documentBuilderFactory().newDocumentBuilder();
7489
return xmlDocumentBuilder.parse(xmlInputSource);
7590
} catch (Exception e) {
7691
throw new RuntimeException(FORMAT_ERROR, e);
7792
}
7893
}
7994

95+
private static DocumentBuilderFactory documentBuilderFactory() {
96+
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
97+
SECURE_FEATURES.forEach((name, value) -> {
98+
try {
99+
factory.setFeature(name, value);
100+
} catch (ParserConfigurationException ignore) {}
101+
});
102+
return factory;
103+
}
104+
80105
private XmlStringPrettyFormatter() {
81106
// utility class
82107
}

assertj-core/src/test/java/org/assertj/core/util/xml/XmlStringPrettyFormatter_prettyFormat_Test.java

Lines changed: 0 additions & 103 deletions
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
/*
2+
* Copyright 2012-2026 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.assertj.tests.core.util.xml;
17+
18+
import static org.assertj.core.api.Assertions.catchException;
19+
import static org.assertj.core.api.BDDAssertions.then;
20+
import static org.assertj.core.util.xml.XmlStringPrettyFormatter.xmlPrettyFormat;
21+
import static org.junit.jupiter.params.provider.Arguments.argumentSet;
22+
23+
import java.math.BigDecimal;
24+
import java.util.List;
25+
26+
import org.junit.jupiter.api.Test;
27+
import org.junit.jupiter.params.ParameterizedTest;
28+
import org.junit.jupiter.params.provider.FieldSource;
29+
import org.junit.jupiter.params.provider.ValueSource;
30+
import org.junitpioneer.jupiter.DefaultLocale;
31+
import org.xml.sax.SAXParseException;
32+
33+
/**
34+
* @author Joel Costigliola
35+
*/
36+
@DefaultLocale("en")
37+
class XmlStringPrettyFormatter_prettyFormat_Test {
38+
39+
private final BigDecimal javaVersion = new BigDecimal(System.getProperty("java.specification.version"));
40+
41+
private final String expected_formatted_xml = javaVersion.compareTo(new BigDecimal("9")) >= 0
42+
? """
43+
<?xml version="1.0" encoding="UTF-8"?><rss version="2.0">
44+
<channel>
45+
<title>Java Tutorials and Examples 1</title>
46+
<language>en-us</language>
47+
</channel>
48+
</rss>
49+
"""
50+
: """
51+
<?xml version="1.0" encoding="UTF-8"?>
52+
<rss version="2.0">
53+
<channel>
54+
<title>Java Tutorials and Examples 1</title>
55+
<language>en-us</language>
56+
</channel>
57+
</rss>
58+
""";
59+
60+
@Test
61+
void should_format_input_prettily() {
62+
// GIVEN
63+
String xmlString = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><rss version=\"2.0\"><channel><title>Java Tutorials and Examples 1</title><language>en-us</language></channel></rss>";
64+
// WHEN
65+
String result = xmlPrettyFormat(xmlString);
66+
// THEN
67+
then(result).isEqualTo(expected_formatted_xml);
68+
}
69+
70+
@Test
71+
void should_format_input_without_xml_declaration_prettily() {
72+
// GIVEN
73+
String xmlString = "<rss version=\"2.0\"><channel><title>Java Tutorials and Examples 1</title><language>en-us</language></channel></rss>";
74+
// WHEN
75+
String result = xmlPrettyFormat(xmlString);
76+
// THEN
77+
if (javaVersion.compareTo(new BigDecimal("9")) >= 0) {
78+
then(result).isEqualTo(expected_formatted_xml.substring("<?xml version='1.0' encoding='UTF-8'?>".length()));
79+
} else {
80+
then(result).isEqualTo(expected_formatted_xml.substring("<?xml version='1.0' encoding='UTF-8'?>\n".length()));
81+
}
82+
}
83+
84+
@Test
85+
void should_format_input_with_space_and_newline_prettily() {
86+
// GIVEN
87+
String xmlString = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><rss version=\"2.0\"><channel> <title>Java Tutorials and Examples 1</title> \n\n<language>en-us</language> </channel></rss>";
88+
// WHEN
89+
String result = xmlPrettyFormat(xmlString);
90+
// THEN
91+
then(result).isEqualTo(expected_formatted_xml);
92+
}
93+
94+
@Test
95+
void should_fail_if_input_is_null() {
96+
// WHEN
97+
Exception exception = catchException(() -> xmlPrettyFormat(null));
98+
// THEN
99+
then(exception).isInstanceOf(IllegalArgumentException.class)
100+
.hasMessageStartingWith("Expecting XML String not to be null");
101+
}
102+
103+
@Test
104+
void should_fail_if_input_is_not_valid() {
105+
// GIVEN
106+
String xmlString = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><rss version=\"2.0\"><channel><title>Java Tutorials and Examples 1</title><language>en-us</language></chnel></rss>";
107+
// WHEN
108+
Exception exception = catchException(() -> xmlPrettyFormat(xmlString));
109+
// THEN
110+
then(exception).isInstanceOf(RuntimeException.class)
111+
.hasMessage("Unable to format XML string")
112+
.hasRootCauseInstanceOf(SAXParseException.class)
113+
.cause()
114+
.hasMessageContaining("The element type \"channel\" must be terminated by the matching end-tag \"</channel>\"");
115+
}
116+
117+
@ParameterizedTest
118+
@ValueSource(strings = {
119+
// Injection into document content
120+
"""
121+
<?xml version="1.0"?>
122+
<!DOCTYPE root [
123+
<!ENTITY xxe SYSTEM "file:///etc/hosts">
124+
]>
125+
<root>&xxe;</root>
126+
""",
127+
// Injection during document parsing
128+
"""
129+
<?xml version="1.0"?>
130+
<!DOCTYPE root [
131+
<!ENTITY % xxe SYSTEM "file:///etc/hosts">
132+
%xxe;
133+
]>
134+
<root>foo</root>
135+
"""
136+
})
137+
void should_fail_if_input_contains_doctype_declaration(String input) {
138+
// WHEN
139+
Exception exception = catchException(() -> xmlPrettyFormat(input));
140+
// THEN
141+
then(exception).isInstanceOf(RuntimeException.class)
142+
.hasMessage("Unable to format XML string")
143+
.cause()
144+
.isInstanceOf(SAXParseException.class)
145+
.hasMessageContaining("DOCTYPE is disallowed");
146+
}
147+
148+
}

0 commit comments

Comments
 (0)