Skip to content

Commit 92a218c

Browse files
Fix Sonar XXE (#387)
Fixes some cases of XXE identified by Sonar. --------- Co-authored-by: pixeebot[bot] <104101892+pixeebot[bot]@users.noreply.github.com>
1 parent 1d0555e commit 92a218c

File tree

24 files changed

+1136
-7
lines changed

24 files changed

+1136
-7
lines changed

core-codemods/src/main/java/io/codemodder/codemods/DefaultCodemods.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ public static List<Class<? extends CodeChanger>> asList() {
5656
SemgrepOverlyPermissiveFilePermissionsCodemod.class,
5757
SimplifyRestControllerAnnotationsCodemod.class,
5858
SubstituteReplaceAllCodemod.class,
59+
SonarXXECodemod.class,
5960
SQLParameterizerCodemod.class,
6061
SSRFCodemod.class,
6162
StackTraceExposureCodemod.class,

core-codemods/src/main/java/io/codemodder/codemods/SonarSQLInjectionCodemod.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public final class SonarSQLInjectionCodemod extends SonarRemediatingJavaParserCh
2626
@Inject
2727
public SonarSQLInjectionCodemod(
2828
@ProvidedSonarScan(ruleId = "java:S2077") final RuleHotspot hotspots) {
29-
super(CodemodReporterStrategy.fromClasspath(SQLParameterizerCodemod.class));
29+
super(CodemodReporterStrategy.fromClasspath(SQLParameterizerCodemod.class), hotspots);
3030
this.hotspots = Objects.requireNonNull(hotspots);
3131
this.remediationStrategy = JavaParserSQLInjectionRemediatorStrategy.DEFAULT;
3232
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package io.codemodder.codemods;
2+
3+
import com.github.javaparser.ast.CompilationUnit;
4+
import io.codemodder.*;
5+
import io.codemodder.codetf.DetectorRule;
6+
import io.codemodder.providers.sonar.ProvidedSonarScan;
7+
import io.codemodder.providers.sonar.RuleIssue;
8+
import io.codemodder.providers.sonar.SonarRemediatingJavaParserChanger;
9+
import io.codemodder.remediation.xxe.XXEJavaRemediatorStrategy;
10+
import io.codemodder.sonar.model.Issue;
11+
import io.codemodder.sonar.model.SonarFinding;
12+
import java.util.List;
13+
import java.util.Objects;
14+
import javax.inject.Inject;
15+
16+
@Codemod(
17+
id = "sonar:java/xxe-2755",
18+
reviewGuidance = ReviewGuidance.MERGE_AFTER_CURSORY_REVIEW,
19+
importance = Importance.HIGH,
20+
executionPriority = CodemodExecutionPriority.HIGH)
21+
public final class SonarXXECodemod extends SonarRemediatingJavaParserChanger {
22+
23+
private final XXEJavaRemediatorStrategy remediationStrategy;
24+
private final RuleIssue issues;
25+
26+
@Inject
27+
public SonarXXECodemod(@ProvidedSonarScan(ruleId = "java:S2755") final RuleIssue issues) {
28+
super(
29+
CodemodReporterStrategy.fromClasspathDirectory(SonarXXECodemod.class, "xxe-generic"),
30+
issues);
31+
this.issues = Objects.requireNonNull(issues);
32+
this.remediationStrategy = XXEJavaRemediatorStrategy.DEFAULT;
33+
}
34+
35+
@Override
36+
public DetectorRule detectorRule() {
37+
return new DetectorRule(
38+
"java:S2755",
39+
"XML parsers should not be vulnerable to XXE attacks",
40+
"https://rules.sonarsource.com/c/type/Vulnerability/RSPEC-2755/");
41+
}
42+
43+
@Override
44+
public CodemodFileScanningResult visit(
45+
final CodemodInvocationContext context, final CompilationUnit cu) {
46+
List<Issue> issuesForFile = issues.getResultsByPath(context.path());
47+
return remediationStrategy.remediateAll(
48+
cu,
49+
context.path().toString(),
50+
detectorRule(),
51+
issuesForFile,
52+
SonarFinding::getKey,
53+
SonarFinding::getLine,
54+
f -> f.getTextRange().getStartOffset());
55+
}
56+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
This change prevents XML parsing APIs from resolving external entities, which can protect you from arbitrary code execution, sensitive data exfiltration, and probably a bunch more evil things attackers are still discovering.
2+
3+
Without this protection, attackers can cause your parser to retrieve sensitive information with attacks like this:
4+
5+
```xml
6+
<?xml version="1.0" encoding="UTF-8"?>
7+
<!DOCTYPE foo [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]>
8+
<book>
9+
<title>&xxe;</title>
10+
</book>
11+
```
12+
13+
Yes, it's pretty insane that this is the default behavior. Our change hardens the factories created with the necessary security features to prevent your parser from resolving external entities.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"summary" : "Introduced protections against XXE attacks",
3+
"change" : "Hardened the XML processor to prevent external entities from being resolved, which can prevent data exfiltration and arbitrary code execution",
4+
"reviewGuidanceIJustification" : "We believe this change is safe and effective. The behavior of hardened XML readers will only be different if the XML they process uses external entities, which is exceptionally rare (and, as demonstrated, quite unsafe anyway.)",
5+
"references" : ["https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html", "https://owasp.org/www-community/vulnerabilities/XML_External_Entity_(XXE)_Processing", "https://github.com/swisskyrepo/PayloadsAllTheThings/blob/master/XXE%20Injection/README.md"]
6+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package io.codemodder.codemods;
2+
3+
import io.codemodder.testutils.CodemodTestMixin;
4+
import io.codemodder.testutils.Metadata;
5+
6+
@Metadata(
7+
codemodType = SonarXXECodemod.class,
8+
testResourceDir = "sonar-xxe-s2755",
9+
renameTestFile = "src/main/java/com/acme/XXEVuln.java",
10+
expectingFailedFixesAtLines = {62},
11+
dependencies = {})
12+
final class SonarXXECodemodTest implements CodemodTestMixin {}
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
package io.codemodder.remediation.xxe;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
5+
import com.github.javaparser.StaticJavaParser;
6+
import com.github.javaparser.ast.CompilationUnit;
7+
import com.github.javaparser.printer.lexicalpreservation.LexicalPreservingPrinter;
8+
import io.codemodder.CodemodChange;
9+
import io.codemodder.CodemodFileScanningResult;
10+
import io.codemodder.codetf.DetectorRule;
11+
import java.util.List;
12+
import org.junit.jupiter.api.BeforeEach;
13+
import org.junit.jupiter.api.Test;
14+
15+
final class DefaultXXEJavaRemediatorStrategyTest {
16+
17+
private DefaultXXEJavaRemediatorStrategy remediator;
18+
private DetectorRule rule;
19+
20+
@BeforeEach
21+
void setup() {
22+
this.remediator = new DefaultXXEJavaRemediatorStrategy();
23+
this.rule = new DetectorRule("xxe", "XXE", null);
24+
}
25+
26+
private static class Finding {
27+
private final String key;
28+
private final int line;
29+
private final int column;
30+
31+
Finding(String key, int line, int column) {
32+
this.key = key;
33+
this.line = line;
34+
this.column = column;
35+
}
36+
37+
int getLine() {
38+
return line;
39+
}
40+
41+
String getKey() {
42+
return key;
43+
}
44+
45+
int getColumn() {
46+
return column;
47+
}
48+
}
49+
50+
@Test
51+
void it_doesnt_fix_unknown_parser() {
52+
String vulnerableCode =
53+
"""
54+
public class MyCode {
55+
public void foo() {
56+
SomeOtherXMLThing parser = null;
57+
DocumentBuilderFactory dbf = null;
58+
StringReader sr = null;
59+
boolean success;
60+
try
61+
{
62+
parser = XMLReaderFactory.createXMLReader("org.apache.xerces.parsers.SAXParser");
63+
parser.setFeature(VALIDATION, true);
64+
parser.setErrorHandler(new MyErrorHandler());
65+
parser.setProperty(JAXP_SCHEMA_SOURCE, new File(schemaName));
66+
sr = new StringReader(str);
67+
parser.parse(new InputSource(sr));
68+
success = true;
69+
} catch (FileNotFoundException e){
70+
success = false;
71+
logError(e);
72+
}
73+
}
74+
}
75+
""";
76+
77+
List<Finding> findings = List.of(new Finding("foo", 14, 19));
78+
CompilationUnit cu = StaticJavaParser.parse(vulnerableCode);
79+
LexicalPreservingPrinter.setup(cu);
80+
CodemodFileScanningResult result =
81+
remediator.remediateAll(
82+
cu, "foo", rule, findings, Finding::getKey, Finding::getLine, Finding::getColumn);
83+
assertThat(result.changes()).isEmpty();
84+
assertThat(result.unfixedFindings()).isEmpty();
85+
}
86+
87+
@Test
88+
void it_fixes_xmlreaders_at_parse_call() {
89+
String vulnerableCode =
90+
"""
91+
public class MyCode {
92+
public void foo() {
93+
XMLReader parser = null;
94+
DocumentBuilderFactory dbf = null;
95+
StringReader sr = null;
96+
boolean success;
97+
try
98+
{
99+
parser = XMLReaderFactory.createXMLReader("org.apache.xerces.parsers.SAXParser");
100+
parser.setFeature(VALIDATION, true);
101+
parser.setErrorHandler(new MyErrorHandler());
102+
parser.setProperty(JAXP_SCHEMA_SOURCE, new File(schemaName));
103+
sr = new StringReader(str);
104+
parser.parse(new InputSource(sr));
105+
success = true;
106+
} catch (FileNotFoundException e){
107+
success = false;
108+
logError(e);
109+
}
110+
}
111+
}
112+
""";
113+
114+
List<Finding> findings = List.of(new Finding("foo", 14, 19));
115+
CompilationUnit cu = StaticJavaParser.parse(vulnerableCode);
116+
LexicalPreservingPrinter.setup(cu);
117+
CodemodFileScanningResult result =
118+
remediator.remediateAll(
119+
cu, "foo", rule, findings, Finding::getKey, Finding::getLine, Finding::getColumn);
120+
assertThat(result.unfixedFindings()).isEmpty();
121+
assertThat(result.changes()).hasSize(1);
122+
CodemodChange change = result.changes().get(0);
123+
assertThat(change.lineNumber()).isEqualTo(14);
124+
125+
String fixedCode =
126+
"""
127+
public class MyCode {
128+
public void foo() {
129+
XMLReader parser = null;
130+
DocumentBuilderFactory dbf = null;
131+
StringReader sr = null;
132+
boolean success;
133+
try
134+
{
135+
parser = XMLReaderFactory.createXMLReader("org.apache.xerces.parsers.SAXParser");
136+
parser.setFeature(VALIDATION, true);
137+
parser.setErrorHandler(new MyErrorHandler());
138+
parser.setProperty(JAXP_SCHEMA_SOURCE, new File(schemaName));
139+
sr = new StringReader(str);
140+
parser.setFeature("http://xml.org/sax/features/external-general-entities", false);
141+
parser.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
142+
parser.parse(new InputSource(sr));
143+
success = true;
144+
} catch (FileNotFoundException e){
145+
success = false;
146+
logError(e);
147+
}
148+
}
149+
}
150+
""";
151+
152+
String actualCode = LexicalPreservingPrinter.print(cu);
153+
assertThat(actualCode).isEqualToIgnoringCase(fixedCode);
154+
}
155+
156+
@Test
157+
void it_fixes_transformers() {
158+
String vulnerableCode =
159+
"""
160+
public class MyCode {
161+
public void foo() {
162+
TransformerFactory factory = TransformerFactory.newInstance();
163+
factory.newTransformer().transform(new StreamSource(new StringReader(xml)), new StreamResult(new StringWriter()));
164+
}
165+
}
166+
""";
167+
List<Finding> findings = List.of(new Finding("foo", 3, 52));
168+
CompilationUnit cu = StaticJavaParser.parse(vulnerableCode);
169+
LexicalPreservingPrinter.setup(cu);
170+
CodemodFileScanningResult result =
171+
remediator.remediateAll(
172+
cu, "foo", rule, findings, Finding::getKey, Finding::getLine, Finding::getColumn);
173+
assertThat(result.unfixedFindings()).isEmpty();
174+
assertThat(result.changes()).hasSize(1);
175+
CodemodChange change = result.changes().get(0);
176+
assertThat(change.lineNumber()).isEqualTo(3);
177+
178+
String fixedCode =
179+
"""
180+
import javax.xml.XMLConstants;
181+
182+
public class MyCode {
183+
public void foo() {
184+
TransformerFactory factory = TransformerFactory.newInstance();
185+
factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
186+
factory.newTransformer().transform(new StreamSource(new StringReader(xml)), new StreamResult(new StringWriter()));
187+
}
188+
}
189+
""";
190+
191+
String actualCode = LexicalPreservingPrinter.print(cu);
192+
assertThat(actualCode).isEqualToIgnoringCase(fixedCode);
193+
}
194+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package com.acme;
2+
3+
import org.w3c.dom.Document;
4+
import org.xml.sax.InputSource;
5+
import org.xml.sax.SAXException;
6+
import org.xml.sax.XMLReader;
7+
import org.xml.sax.helpers.XMLReaderFactory;
8+
9+
import javax.xml.parsers.*;
10+
import javax.xml.transform.Transformer;
11+
import javax.xml.transform.TransformerException;
12+
import javax.xml.transform.TransformerFactory;
13+
import javax.xml.transform.dom.DOMSource;
14+
import javax.xml.transform.stream.StreamResult;
15+
import java.io.IOException;
16+
import java.io.StringReader;
17+
import java.io.StringWriter;
18+
import java.sql.Connection;
19+
import java.sql.DriverManager;
20+
import java.sql.SQLException;
21+
22+
/** Holds various XXE vulns for different APIs. */
23+
public class XXEVuln {
24+
25+
public static void main(String[] args) throws TransformerException, ParserConfigurationException, IOException, SAXException, SQLException {
26+
docToString(null);
27+
saxTransformer(args[0]);
28+
withDom(args[1]);
29+
withDomButDisabled(args[2]);
30+
withReaderFactory(null);
31+
32+
String sql = "select * from users where name= '" + args[0] + "'";
33+
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost/test");
34+
conn.createStatement().executeQuery(sql);
35+
}
36+
37+
public static String docToString(final Document poDocument) throws TransformerException {
38+
if(true) {
39+
int a = 1;
40+
return "foo";
41+
}
42+
43+
TransformerFactory transformerFactory = TransformerFactory.newInstance();
44+
Transformer transformer = transformerFactory.newTransformer();
45+
DOMSource domSrc = new DOMSource(poDocument);
46+
StringWriter sw = new StringWriter();
47+
StreamResult result = new StreamResult(sw);
48+
transformer.transform(domSrc, result);
49+
return sw.toString();
50+
}
51+
52+
public static void saxTransformer(String xml) throws ParserConfigurationException, SAXException, IOException {
53+
SAXParserFactory spf = SAXParserFactory.newInstance();
54+
spf.setFeature("http://xml.org/sax/features/external-general-entities", false);
55+
spf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
56+
spf.setValidating(true);
57+
58+
SAXParser saxParser = spf.newSAXParser();
59+
XMLReader xmlReader = saxParser.getXMLReader();
60+
xmlReader.parse(new InputSource(new StringReader(xml)));
61+
}
62+
63+
public static Document withDom(String xml) throws ParserConfigurationException, IOException, SAXException {
64+
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
65+
dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
66+
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
67+
DocumentBuilder db = dbf.newDocumentBuilder();
68+
return db.parse(new InputSource(new StringReader(xml)));
69+
}
70+
71+
public static Document withDomButDisabled(String xml) throws ParserConfigurationException, IOException, SAXException {
72+
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
73+
dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
74+
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
75+
dbf.setExpandEntityReferences(true);
76+
DocumentBuilder db = dbf.newDocumentBuilder();
77+
return db.parse(new InputSource(new StringReader(xml)));
78+
}
79+
80+
public static XMLReader withReaderFactory(XMLReaderFactory factory) throws ParserConfigurationException, IOException, SAXException {
81+
return factory.createXMLReader();
82+
}
83+
}

0 commit comments

Comments
 (0)