3

A recent Salesforce Security review flagged the case of inserting this directly into a database text field:

<h1>Hello</h1>
<img src=x onerror=alert(document.cookie)>
<h1>World</h1>

causing this to deliver the javaScript to the browser that then executes it:

<apex:outputText escape="false" value="{! text }"/>

(The use case here is that we want dynamically generated HTML to be presented to the client with the HTML interpreted as HTML and not escaped.)

Answers to questions like this Filtering JavaScript out of HTML argue for a whitelisting approach where you parse the HTML and only keep the "known good" parts so discarding the JavaScript.

Is there any pre-existing Apex code of that form, either in the Salesforce APIs or as open source?

2 Answers 2

2

There isn't a library available that I know of, but it's actually pretty easy. Use Dom.Document to load the fragment as XML, then parse it using something like this:

static Map<String, Set<String>> safety = new Map<String, Set<String>> {
    'p' => new Set<String> { 'class' },
    'div' => new Set<String> { 'class' },
    ...
};

public static void purifyChildren(Dom.XmlNode parent) {
    Dml.XmlNode[] children = parent.getChildren();
    for(Integer i = children.size()-1; i >= 0; i--) {
        Dml.XmlNode node child = children[i];
        // Element not allowed, e.g. "script"
        if(!safety.containsKey(child.getName().toLowerCase())) {
            parent.removeChild(child);
        } else {
            // Recursively fix children
            purifyChildren(child);
        }
    }
    String parentName = parent.getName().tolowerCase();
    if(parent.getParentNode() == null || parent.getName() == null) {
        return; // Don't worry about this node, it's the root or a text node
    }
    for(Integer i = parent.getAttributeCount() - 1; i >= 0; i--) {
        String key = parent.getAttributeKeyAt(i), ns = parent.getAttributeKeyNsAt(i);
        if(!safety.get(parentName).contains(key.toLowerCase())) {
            parent.removeAttribute(key, ns);
        }
    }
}

You do need to specify which tags and attributes to allow, but this is up to you. The final code looks like:

Dom.Document doc = new Dom.Document();
doc.load('<root>'+fragment+'</root>');
purify(fragment.getRootElement());
String result = doc.toXmlString().removeStart('<root>').removeEnd('</root>');

Note: I haven't tested this, but it should be a decent starting point. All you need to do is modify the filters to limit the whitelist to what you need.

5
  • Nice coding; I particularly like removing rather than adding into a new tree. Working out what to include in the safety map is point to be a bit tricky though. I'll need to check what the HTML editor we are using allows through.
    – Keith C
    Commented Nov 17, 2020 at 19:58
  • Wondering if this approach will actually work with HTML (SGML) rather than XHTML? I suspect not. Note how the img element and its attributes in the original post are in HTML form rather than XHTML form.
    – Phil W
    Commented Nov 17, 2020 at 21:52
  • @PhilW It will work generic cases, I think the only weird part is BR/HR and the fact that HTML allows missing end tags. That said, what I would personally do is reject any non-well-formed HTML automatically and give the user an error (the editor should always generate well-formed HTML).
    – sfdcfox
    Commented Nov 17, 2020 at 21:55
  • You are actually saying you only accept XHTML. HTML is based on SGML which allows for non-closed tags and for attributes without quotes. This may be a problem for Keith C depending how the HTML is generated.
    – Phil W
    Commented Nov 17, 2020 at 22:00
  • @PhilW Sort of. "Real" HTML generated by any reasonable editor will always have quoted attributes (unless it's a Boolean), but XHTML is even stricter, as it strictly conforms to XML. You're right it won't work for SGML, but it should work for most reasonable implementations of HTML.
    – sfdcfox
    Commented Nov 17, 2020 at 22:04
1

This is the code we use for this purpose that relies on using regex. The benefit of this approach is that it works with both HTML and XHTML forms of markup. Note that it does more than just remove javascript since there can be other "dangerous" side-effects with various elements in the page. Note that any body content for the removed tags is retained and becomes simply text (this would mean, for example, that an inline stylesheet's CSS will appear as text).

First the tag stripping bit:

     public static String stripTags(String text, Boolean onlyDangerous) {
         if (text == null) {
             return '';
         }

        String patternToRemove;

        if (onlyDangerous) {
            patternToRemove = '(</?[^(p|ol|ul|li|span|i|b|br|div|a|blockquote|code|h1|h2|h3|h4|h5|img|pre|s|sub|strong|hr|table|tr|th|tbody|theader|input|select|form|button)]/?>)';
        } else {
            patternToRemove = '<.*?>';
        }

        return stripPattern(text, patternToRemove);
    }

    public static String stripPattern(String text, String sPattern) {
        Pattern myPattern = Pattern.compile(sPattern);
        Matcher myMatcher = myPattern.matcher(text);

        return myMatcher.replaceAll('');
    }

Adding stripping of the "on*" attributes is done by basically running a double pass of the pattern stripper, using an extended piece of processing like:

public static String stripTagsAndOnAttributes(String text) {
    String cleaned = stripTags(text, true);

    return stripPattern(cleaned, '(?<=<.{1,100}?[^>])(\\s+on[-a-zA-Z0-9_]+\\s*=[^\\s/>]*)');
}

Note that I have cheated a little here: the stripping of on attributes assumes:

  1. Case sensitive attribute names
  2. That there's no more than 100 characters between the attribute and the start of the tag that contains it. Adjust as you see fit.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .