cure53 / DOMPurify

DOMPurify - a DOM-only, super-fast, uber-tolerant XSS sanitizer for HTML, MathML and SVG. DOMPurify works with a secure default, but offers a lot of configurability and hooks. Demo:
https://cure53.de/purify
Other
14.15k stars 735 forks source link

Latest versions of DOMPurify 2.5.x block custom SVG elements when they are set via ADD_TAGS config. #954

Closed kevinroast closed 6 months ago

kevinroast commented 6 months ago

Background & Context

Our use-case works with custom SVG tags (output by MS Visio export process). These tags are passed via the ADD_TAGS configuration during DOMPurify set-up.

We were using an older DOMPurify (2.0.8) and the custom tags supplied via ADD_TAGS config work as expected and survive the purify process. With latest DOMPurify (2.5.2) which we obviously want for security updates, the custom tags are stripped.

Bug

Latest versions of DOMPurify 2.5.x block custom SVG elements when they are set via ADD_TAGS config.

Input

Example SVG HTML input:

<!-- Generated by Microsoft Visio, SVG Export automation.svg Page-1 -->
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:ev="http://www.w3.org/2001/xml-events"
        xmlns:v="http://schemas.microsoft.com/visio/2003/SVGExtensions/" width="5.65654in" height="0.711985in"
        viewBox="0 0 407.271 51.2629" xml:space="preserve" color-interpolation-filters="sRGB" class="st5">
    <v:documentProperties v:langID="2057" v:metric="true" v:viewMarkup="false"/>
    <g v:mID="0" v:index="1" v:groupContext="foregroundPage">
        <title>Page-1</title>
        <v:pageProperties v:drawingScale="0.0393701" v:pageScale="0.0393701" v:drawingUnits="24" v:shadowOffsetX="8.50393" v:shadowOffsetY="-8.50393"/>
        <g id="group1-1" transform="translate(0.25,-0.25)" v:mID="1" v:groupContext="group">
            <v:custProps>
                <v:cp v:nameU="Row_3" v:lbl="scomId" v:type="0" v:langID="1033" v:val="VT4(d706f0fd-61f1-4a87-277a-928ddb1a76b6)"/>
                <v:cp v:nameU="Row_4" v:lbl="scomName" v:type="0" v:langID="2057" v:val="VT4(IIS 10 Computer Group)"/>
                <v:cp v:nameU="Row_5" v:lbl="scomPath" v:type="0" v:langID="2057"/>
            </v:custProps>
        </g>
    </g>
</svg>

Code is something like this, we generate the list of custom tags from input, so you would expect 'customTags' to be something like: 0: "v:documentProperties" 1: "v:pageProperties" 2: "v:custProps" 3: "v:cp"

const config = { ADD_TAGS: customTags }; const output = DOMPurify.sanitize(html, config);

Given output

<svg class="st5" color-interpolation-filters="sRGB" xml:space="preserve" viewBox="0 0 407.271 51.2629" height="0.711985in" width="5.65654in" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg">
    <g v:groupcontext="foregroundPage" v:index="1" v:mid="0">
        <title>Page-1</title>
        <g v:groupcontext="group" v:mid="1" transform="translate(0.25,-0.25)" id="group1-1">
        </g>
    </g>
</svg>

Expected output

<svg class="st5" color-interpolation-filters="sRGB" xml:space="preserve" viewBox="0 0 407.271 51.2629" height="0.711985in" width="5.65654in" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg">
    <v:documentproperties v:viewmarkup="false" v:metric="true" v:langid="2057"></v:documentproperties>
    <g v:groupcontext="foregroundPage" v:index="1" v:mid="0">
        <title>Page-1</title>
        <v:pageproperties v:shadowoffsety="-8.50393" v:shadowoffsetx="8.50393" v:drawingunits="24" v:pagescale="0.0393701" v:drawingscale="0.0393701"></v:pageproperties>
        <g v:groupcontext="group" v:mid="1" transform="translate(0.25,-0.25)" id="group1-1">
            <v:custprops>
                <v:cp v:val="VT4(d706f0fd-61f1-4a87-277a-928ddb1a76b6)" v:langid="1033" v:lbl="scomId" v:nameu="Row_3"></v:cp>
                <v:cp v:val="VT4(IIS 10 Computer Group)" v:langid="2057" v:type="0" v:lbl="scomName" v:nameu="Row_4"></v:cp>
                <v:cp v:langid="2057" v:type="0" v:lbl="scomPath" v:nameu="Row_5"></v:cp>
            </v:custprops>
        </g>
    </g>
</svg>

Solution

I was able to solve the issue directly in purify.js source as follows: image Obviously I am not sure this is the correct solution as I don't know the code well enough to be sure, but it solved the issue for us as it appears that the ADD_TAGS config is no longer being respected for SVG element input.

cure53 commented 6 months ago

Alright, I had a closer look and found the issue. The problem is that we at some point started limited the permitted namespaces, and in your case, the XML contains one that is not allow-listed.

I believe you can fix the issue elegantly with a config option:

var html = `<!-- Generated by Microsoft Visio, SVG Export automation.svg Page-1 -->
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:ev="http://www.w3.org/2001/xml-events"
        xmlns:v="http://schemas.microsoft.com/visio/2003/SVGExtensions/" width="5.65654in" height="0.711985in"
        viewBox="0 0 407.271 51.2629" xml:space="preserve" color-interpolation-filters="sRGB" class="st5">
    <v:documentProperties v:langID="2057" v:metric="true" v:viewMarkup="false"/>
    <g v:mID="0" v:index="1" v:groupContext="foregroundPage">
        <title>Page-1</title>
        <v:pageProperties v:drawingScale="0.0393701" v:pageScale="0.0393701" v:drawingUnits="24" v:shadowOffsetX="8.50393" v:shadowOffsetY="-8.50393"/>
        <g id="group1-1" transform="translate(0.25,-0.25)" v:mID="1" v:groupContext="group">
            <v:custProps>
                <v:cp v:nameU="Row_3" v:lbl="scomId" v:type="0" v:langID="1033" v:val="VT4(d706f0fd-61f1-4a87-277a-928ddb1a76b6)"/>
                <v:cp v:nameU="Row_4" v:lbl="scomName" v:type="0" v:langID="2057" v:val="VT4(IIS 10 Computer Group)"/>
                <v:cp v:nameU="Row_5" v:lbl="scomPath" v:type="0" v:langID="2057"/>
            </v:custProps>
        </g>
    </g>
</svg>`;

var config = { 
  ADD_TAGS: ['v:documentProperties', 'v:pageProperties'], 
  PARSER_MEDIA_TYPE: 'application/xhtml+xml', 
  ALLOWED_NAMESPACES: ['http://www.w3.org/2000/svg','http://schemas.microsoft.com/visio/2003/SVGExtensions/'] 
}; 
var output = DOMPurify.sanitize(html, config);

Fot this example, I allow-listed two elements and added their namespace, that seems to do the trick :slightly_smiling_face:

cure53 commented 6 months ago

Closing for now, as I assume this is solved

kevinroast commented 6 months ago

Alright, I had a closer look and found the issue. The problem is that we at some point started limited the permitted namespaces, and in your case, the XML contains one that is not allow-listed.

I believe you can fix the issue elegantly with a config option:

Fot this example, I allow-listed two elements and added their namespace, that seems to do the trick 🙂

Thanks for taking the time to investigate the issue. I will give this config solution a try and let you know!

kevinroast commented 6 months ago

After some testing I can say that it is close but not quite the same as the previous processing.

With a more involved SVG example, it is removing CDATA blocks inside embedded style elements and also removing the important viewBox attribute from the root SVG element.

Example Input:

<!-- Generated by Microsoft Visio, SVG Export automation.svg Page-1 -->
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:ev="http://www.w3.org/2001/xml-events"
        xmlns:v="http://schemas.microsoft.com/visio/2003/SVGExtensions/" width="5.65654in" height="0.711985in"
        viewBox="0 0 407.271 51.2629" xml:space="preserve" color-interpolation-filters="sRGB" class="st5">
    <v:documentProperties v:langID="2057" v:metric="true" v:viewMarkup="false"/>
    <style type="text/css">
    <![CDATA[
        .st5 {fill:none;fill-rule:evenodd;font-size:12px;overflow:visible;stroke-linecap:square;stroke-miterlimit:3}
    ]]>
    </style>
    <g v:mID="0" v:index="1" v:groupContext="foregroundPage">
        <title>Page-1</title>
        <v:pageProperties v:drawingScale="0.0393701" v:pageScale="0.0393701" v:drawingUnits="24" v:shadowOffsetX="8.50393" v:shadowOffsetY="-8.50393"/>
        <g id="group1-1" transform="translate(0.25,-0.25)" v:mID="1" v:groupContext="group">
            <v:custProps>
                <v:cp v:nameU="Row_3" v:lbl="scomId" v:type="0" v:langID="1033" v:val="VT4(d706f0fd-61f1-4a87-277a-928ddb1a76b6)"/>
                <v:cp v:nameU="Row_4" v:lbl="scomName" v:type="0" v:langID="2057" v:val="VT4(IIS 10 Computer Group)"/>
                <v:cp v:nameU="Row_5" v:lbl="scomPath" v:type="0" v:langID="2057"/>
            </v:custProps>
        </g>
    </g>
</svg>

The STYLE element CDATA block and the SVG element viewBox attribute are missing from the output after processing the above input with the suggested config.

Is there a suggested configuration that can maintain those values? Thanks!

kevinroast commented 6 months ago

Do you think this can be solved by config, if not I can raise another issue showing where the processing is now different...?

cure53 commented 6 months ago

Yup, this should do the trick:

var config = { 
  ADD_TAGS: ['v:documentProperties', 'v:pageProperties'], // from former example, needs to be updated
  PARSER_MEDIA_TYPE: 'application/xhtml+xml', 
  ADD_ATTR: ['viewBox'], // newly added
  ADD_TAGS: ['#cdata-section'], // newly added
  ALLOWED_NAMESPACES: ['http://www.w3.org/2000/svg','http://schemas.microsoft.com/visio/2003/SVGExtensions/'] 
};