Skip to content

Commit c8f5e53

Browse files
committed
add method to validate a XML against SVG format
1 parent 8d9a444 commit c8f5e53

7 files changed

Lines changed: 127 additions & 0 deletions

File tree

pom.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,16 @@
8484
<artifactId>owasp-java-html-sanitizer</artifactId>
8585
<version>20260102.1</version>
8686
</dependency>
87+
<dependency>
88+
<groupId>org.apache.xmlgraphics</groupId>
89+
<artifactId>batik-dom</artifactId>
90+
<version>1.19</version>
91+
</dependency>
92+
<dependency>
93+
<groupId>org.apache.xmlgraphics</groupId>
94+
<artifactId>batik-anim</artifactId>
95+
<version>1.19</version>
96+
</dependency>
8797
<!-- TEST ONLY PURPOSE -->
8898
<dependency>
8999
<groupId>org.junit.jupiter</groupId>

src/main/java/eu/righettod/SecurityUtils.java

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33

44
import com.auth0.jwt.interfaces.DecodedJWT;
5+
import org.apache.batik.anim.dom.SAXSVGDocumentFactory;
6+
import org.apache.batik.util.XMLResourceDescriptor;
57
import org.apache.commons.csv.CSVFormat;
68
import org.apache.commons.csv.CSVRecord;
79
import org.apache.commons.imaging.ImageInfo;
@@ -35,6 +37,7 @@
3537
import org.owasp.html.HtmlPolicyBuilder;
3638
import org.owasp.html.PolicyFactory;
3739
import org.w3c.dom.Document;
40+
import org.w3c.dom.svg.SVGDocument;
3841
import org.xml.sax.EntityResolver;
3942
import org.xml.sax.InputSource;
4043
import org.xml.sax.SAXException;
@@ -64,6 +67,7 @@
6467
import java.nio.charset.Charset;
6568
import java.nio.charset.StandardCharsets;
6669
import java.nio.file.Files;
70+
import java.nio.file.Paths;
6771
import java.security.MessageDigest;
6872
import java.security.SecureRandom;
6973
import java.time.Duration;
@@ -1634,4 +1638,62 @@ public static String sanitizeLogMessage(String message, int maxMessageLength) {
16341638

16351639
return sanitized;
16361640
}
1641+
1642+
/**
1643+
* Identify if an XML is an SVG image.<br>
1644+
* The goal of this method is to prevent to leverage SVG, as an vector, to achieve a XSS when XML format is accepted.<br>
1645+
* Leverage <a href="https://xmlgraphics.apache.org/batik/">Apache Batik</a> to delegate the parsing and support for the SVG format.<br><br>
1646+
* <b>Due to the intended usage of the method, the following choice were made:</b>
1647+
* <ul>
1648+
* <li>Raise an exception when a non SVG related external references is identified.</li>
1649+
* <li>Throw any exception that can occur if the provided content is invalid like for example an invalid XML file or a non existing file.</li>
1650+
* <li>Explicitly check the XML prior to pass it to Batik even if Batik seems not prone to XXE/SSRF classes of vulnerability.</li>
1651+
* </ul>
1652+
*
1653+
* @param xmlFilePath Filename of the XML file to check.
1654+
* @return True only if XML is an valid SVG image.
1655+
* @throws SecurityException If a non SVG external references is detected into the XML content.
1656+
* @throws Exception If a error occur due to an invalid content provided.
1657+
* @see "https://developer.mozilla.org/en-US/docs/Web/SVG"
1658+
* @see "https://www.fortinet.com/blog/threat-research/scalable-vector-graphics-attack-surface-anatomy"
1659+
* @see "https://portswigger.net/web-security/cross-site-scripting"
1660+
* @see "https://xmlgraphics.apache.org/batik/"
1661+
* @see "https://github.com/apache/xmlgraphics-batik/blob/main/batik-dom/src/main/java/org/apache/batik/dom/util/SAXDocumentFactory.java#L420"
1662+
* @see "https://portswigger.net/web-security/xxe"
1663+
* @see "https://portswigger.net/web-security/ssrf"
1664+
*/
1665+
public static boolean isXMLSVGImage(String xmlFilePath) throws Exception {
1666+
boolean isSvg = true;
1667+
List<String> svgValidSystemIDs = List.of("http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd", "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11-basic.dtd", "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11-tiny.dtd", "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd");
1668+
1669+
//Load the XML content into a reader
1670+
String xmlContent = Files.readString(Paths.get(xmlFilePath));
1671+
//Then ensure that the XML document does not contains any non SVG external references
1672+
try (Reader reader = StringReader.of(xmlContent)) {
1673+
DocumentBuilderFactory xmlFactory = DocumentBuilderFactory.newInstance();
1674+
DocumentBuilder docBuilder = xmlFactory.newDocumentBuilder();
1675+
docBuilder.setEntityResolver((publicId, systemId) -> {
1676+
if (systemId != null && !svgValidSystemIDs.contains(systemId)) {
1677+
throw new SecurityException("External references detected: " + systemId);
1678+
}
1679+
return new InputSource(new ByteArrayInputStream("".getBytes()));
1680+
});
1681+
docBuilder.parse(new InputSource(reader));
1682+
}
1683+
//Then parse the XML with Apache Batik
1684+
try (Reader reader = StringReader.of(xmlContent)) {
1685+
//Method SAXDocumentFactory.createDocument() do not load external DTD or entities.
1686+
String parserClassName = XMLResourceDescriptor.getXMLParserClassName();
1687+
SAXSVGDocumentFactory svgFactory = new SAXSVGDocumentFactory(parserClassName);
1688+
//Method svgFactory.createSVGDocument() raise an IO exception if the XML is not a valid SVG image
1689+
try {
1690+
SVGDocument doc = svgFactory.createSVGDocument(null, reader);
1691+
isSvg = (doc != null && doc.getRootElement() != null);
1692+
} catch (IOException e) {
1693+
isSvg = false;
1694+
}
1695+
}
1696+
1697+
return isSvg;
1698+
}
16371699
}

src/test/java/eu/righettod/TestSecurityUtils.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -806,5 +806,33 @@ public void sanitizeLogMessage() {
806806
assertEquals(expectedSanitizedMessage, sanitizedMessage);
807807
});
808808
}
809+
810+
@Test
811+
public void isXMLSVGImage() throws Exception {
812+
final String msgErrorIncorrectDetectionTemplate = "File must be detected as a %s SVG image!";
813+
//Normal XML
814+
String testFile = getTestFilePath("test-svg-nonsvg.xml");
815+
boolean isSvg = SecurityUtils.isXMLSVGImage(testFile);
816+
assertFalse(isSvg, String.format(msgErrorIncorrectDetectionTemplate, "invalid"));
817+
//Valid SVG with SVG external references: SVG < 2.0
818+
testFile = getTestFilePath("test-svg-valid-with-svg-dtd.xml");
819+
isSvg = SecurityUtils.isXMLSVGImage(testFile);
820+
assertTrue(isSvg, String.format(msgErrorIncorrectDetectionTemplate, "valid"));
821+
//Valid SVG without SVG external references: SVG 2.0
822+
testFile = getTestFilePath("test-svg-valid-without-svg-dtd.xml");
823+
isSvg = SecurityUtils.isXMLSVGImage(testFile);
824+
assertTrue(isSvg, String.format(msgErrorIncorrectDetectionTemplate, "valid"));
825+
//Valid SVG but with an external XXE reference
826+
testFile = getTestFilePath("test-svg-valid-with-external-xxe.xml");
827+
try {
828+
SecurityUtils.isXMLSVGImage(testFile);
829+
fail(String.format(msgErrorIncorrectDetectionTemplate, "invalid"));
830+
} catch (SecurityException e) {
831+
if (!e.getMessage().startsWith("External references detected:")) {
832+
fail(String.format(msgErrorIncorrectDetectionTemplate, "invalid"));
833+
}
834+
//Otherwise it is expected.
835+
}
836+
}
809837
}
810838

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?xml version="1.0" standalone="no"?>
2+
<note>
3+
<to>Tove</to>
4+
<from>Jani</from>
5+
<heading>Reminder</heading>
6+
<body>Don't forget me this weekend!</body>
7+
</note>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE svg [
3+
<!ENTITY xxe SYSTEM "https://evil.com">
4+
]>
5+
<svg width="200" height="200" xmlns="http://www.w3.org/2000/svg">
6+
<text x="10" y="20">&xxe;</text>
7+
</svg>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?xml version="1.0" standalone="no"?>
2+
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
3+
4+
<svg version="1.1" baseProfile="full" xmlns="http://www.w3.org/2000/svg">
5+
<rect width="300" height="100" style="fill:rgb(0,0,255);stroke-width:3;stroke:rgb(0,0,0)" />
6+
<script type="text/javascript">alert(window.location.href);</script>
7+
</svg>
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="120" viewBox="0 0 200 120">
2+
<rect x="10" y="10" width="180" height="100" rx="12" fill="#4CAF50"/>
3+
<text x="100" y="70" text-anchor="middle" dominant-baseline="middle" font-family="system-ui, sans-serif" font-size="20" fill="white">
4+
SVG 2.0
5+
</text>
6+
</svg>

0 commit comments

Comments
 (0)