Skip to content

XML parser does not support DOCTYPE with <!ENTITY> declarations #2486

Closed

Description

@nramc

When Jsoup.clean() processes XML/SVG input that contains a DOCTYPE declaration with an internal DTD subset (i.e., entity declarations enclosed in [...]), jsoup correctly removes the DOCTYPE tag itself but leaves the closing ]> of the subset in the output string.

This was discovered while sanitizing SVG files that follow the Adobe Illustrator export format.

jsoup version: 1.22.1
Java version: 25

Steps to Reproduce

Call Jsoup.clean() with the following input SVG that contains a DOCTYPE with multiple <!ENTITY> declarations inside an internal DTD subset:

import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.safety.Safelist;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

class SvgSanitizerTest {
    Safelist safelist = Safelist.none()
            .addTags("svg", "circle")
            .addAttributes("svg", "viewBox")
            .addAttributes("circle", "cx", "cy", "r", "fill");


    @Test
    void shouldRemoveDocTypeTag() {
        String dirtySvg = """
                <?xml version="1.0" encoding="utf-8"?>
                <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
                	<!ENTITY ns_extend "http://ns.adobe.com/Extensibility/1.0/">
                	<!ENTITY ns_ai "http://ns.adobe.com/AdobeIllustrator/10.0/">
                	<!ENTITY ns_graphs "http://ns.adobe.com/Graphs/1.0/">
                	<!ENTITY ns_vars "http://ns.adobe.com/Variables/1.0/">
                	<!ENTITY ns_imrep "http://ns.adobe.com/ImageReplacement/1.0/">
                	<!ENTITY ns_sfw "http://ns.adobe.com/SaveForWeb/1.0/">
                	<!ENTITY ns_custom "http://ns.adobe.com/GenericCustomNamespace/1.0/">
                	<!ENTITY ns_adobe_xpath "http://ns.adobe.com/XPath/1.0/">
                ]>
                <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
                  <circle cx="50" cy="50" r="40" fill="red"/>
                </svg>
                """;
        String expectedSvg = """
                <svg viewBox="0 0 100 100">
                      <circle cx="50" cy="50" r="40" fill="red" />
                  </svg>
                """;

        Document.OutputSettings outputSettings = new Document.OutputSettings()
                .syntax(Document.OutputSettings.Syntax.xml)
                .prettyPrint(false);

        var cleanedSvg = Jsoup.clean(dirtySvg, "", safelist, outputSettings);

        assertThat(cleanedSvg).isEqualToNormalizingWhitespace(expectedSvg);
    }

}

Expected Behaviour

The entire DOCTYPE block (including its internal subset [...]>) is removed. Output:

<svg viewBox="0 0 100 100">
  <circle cx="50" cy="50" r="40" fill="red" />
</svg>

Actual Behaviour

The DOCTYPE declaration is removed but the closing ]> of the internal DTD subset is left behind in the output:

]&gt;
<svg viewBox="0 0 100 100">
  <circle cx="50" cy="50" r="40" fill="red" />
</svg>

Impact

Any application using Jsoup.clean() to sanitize SVG (or XML) produced by tools like Adobe Illustrator will silently receive malformed output containing the ]> artifact, which can break downstream XML parsers and SVG renderers.

Could you please have a look into this please ?
Kindly let me know if you need any more details about the issue.
Many thanks in advance!!

BR
Ram

Activity

  1. jhy commented on Apr 17, 2026

    @jhy
    Owner

    Hi,

    I had a look at this. The issue you are seeing is not from the Cleaner per se but the HTML and XML parser.

    In HTML those internal subset entities are not supported in a doctype, and so when the parser sees the first it skips them according to the bogus doctype rules in the spec.

    With the way you are using the jsoup cleaner, you are using the HTML parser. And this HTML is the expected output. (And you can confirm same by saving the file as .html and opening in Chrome.)

    However, configuring it to use the XML parser won't help in the current form, because our XML parser actually uses the same doctype parsing state machine and will get the same output.

    I'm actually a bit surprised to see entities like this in the wild, given e.g. the Billion Laughs Attack and how people generally disable entity support in XML parsers. How prevalent is it for your data? You might be able to workaround by doing some pre or post cleaning.

    E.g. you could use the Cleaner#clean(Document) form of the cleaner with Jsoup#Parse(xml, XmlParser) providing the input doc parsed as XML (not HTML). Then you could just clean.select("svg") to get the SVG element (cleaned)

    I will have a think about if we want to add some better support in the XML parser for these - perhaps just a passthrough such that we parse the doctype correctly and store the internal entities for serialization, but not actually expanding or otherwise supporting the entities.

  2. changed the title [-]Jsoup.clean() leaves ]> characters when DOCTYPE contains DTD subset with multiple <!ENTITY> declarations[/-] [+]XML parser does not support DOCTYPE with <!ENTITY> declarations[/+] on Apr 17, 2026
  3. added 2 commits that reference this issue on Apr 18, 2026
    c62bf19
    1df14ed
  4. added
    bugA confirmed bug, that we should fix
    fixedAn {bug|improvement} that has been {fixed|implemented}
    on Apr 18, 2026
  5. added this to the 1.22.2 milestone on Apr 18, 2026
  6. jhy commented on Apr 18, 2026

    @jhy
    Owner

    OK, I added pass through support for doctype internal subsets so that these entities would be preserved.

    Here's a fuller example of how you can use the Cleaner set up for XML input / output:

    String svgXml = """
    <?xml version="1.0" encoding="utf-8"?>
    <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
    <!ENTITY ns_extend "http://ns.adobe.com/Extensibility/1.0/">
    ]>
    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
      <circle cx="50" cy="50" r="40" fill="red" onclick="alert(1)"/>
    </svg>
    """;
    
    Safelist svgSafelist = Safelist.none()
        .addTags("svg", "circle")
        .addAttributes("svg", "xmlns", "viewBox")
        .addAttributes("circle", "cx", "cy", "r", "fill");
    
    Document dirty = Jsoup.parse(svgXml, "", Parser.xmlParser());
    Element svg = dirty.selectFirst("svg");
    
    Document shell = Document.createShell("");
    shell.outputSettings()
        .syntax(Document.OutputSettings.Syntax.xml)
        .prettyPrint(false);
    shell.body().appendChild(svg.clone());
    
    Document clean = new Cleaner(svgSafelist).clean(shell);
    String cleanedSvg = clean.selectFirst("svg").outerHtml();

    Gives:

    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><circle cx="50" cy="50" r="40" fill="red" /></svg>

    Because the Cleaner is body fragment orientated (for now; #2284 will make it simpler when working at a outer Document level) we jump through a shell body.

  7. nramc commented on Apr 22, 2026

    @nramc
    Author

    thank you so much, @jhy !!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugA confirmed bug, that we should fixfixedAn {bug|improvement} that has been {fixed|implemented}

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions