configurable safe_html-transform with attribute-filtering

BAG-Postmaster <[email protected]>
Newsgroups gmane.comp.web.zope.plone.archetypes.devel
Message-ID <[email protected]>
This is a extended version of safe_html. It would be nice to get it into
PortalTransforms for Plone 2.2, maybe as an additional configurable_safe_html.

It works like this:
- Filter out tags:
   - a tag whitelist (valid_tags - same as the original safe_html,
     only the tags are deleted. All child nodes are removed if tag is
     in nasty_tags)
- Filter out attributes:
   - a attribute whitelist (tags/attributes pairs)
   - a attribute blacklist (tags/attributes pairs)
   (dicts of tags and attributes ({ 'p,div,b': 'style, align, bgcolor'}).
- option to remove javascript (combined tag/attrib/attrib-value filter)
- option to disable transform completely (no need to remove it from
   portal_transforms and loose all config-settings)

All the options are safed with PortalTransforms' own
configuration-mechanisms. The options are mapped into dicts inside convert()
(should be done in __init__, but i can't get the current config-settings
there, kwargs is empty).

My plans are to write some unittests within the next two weeks and make a 
Product with a configlet for use with plone 2.1.

Comments welcome.


..carsten
safe_html.py (text/plain, 11.2 KB)
from Products.PortalTransforms.interfaces import itransform
from Products.CMFDefault.utils import bodyfinder
from Products.CMFDefault.utils import IllegalHTML
from zLOG import LOG, PROBLEM

from sgmllib import SGMLParser
from Products.CMFDefault.utils import SimpleHTMLParser
from Products.CMFDefault.utils import VALID_TAGS
from Products.CMFDefault.utils import NASTY_TAGS

# tag mapping: tag -> short or long tag
VALID_TAGS = VALID_TAGS.copy()
NASTY_TAGS = NASTY_TAGS.copy()

# add some tags to allowed types. This should be fixed in CMFDefault
VALID_TAGS['ins'] = 1
VALID_TAGS['del'] = 1
VALID_TAGS['q'] = 1

msg_pat = """
<div class="system-message">
<p class="system-message-title">System message: %s</p>
%s</d>
"""

class StrippingParser(SGMLParser):
    """Pass only allowed tags;  raise exception for known-bad.
    
    Copied from Products.CMFDefault.utils
    Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
    """

    from htmlentitydefs import entitydefs # replace entitydefs from sgmllib

    def __init__(self, valid, nasty,
                 attrib_whitelist, attrib_blacklist,
                 remove_javascript, raise_error):
        
        SGMLParser.__init__( self )
        self.result = []
        self.valid = valid
        self.nasty = nasty
        self.attrib_whitelist = attrib_whitelist
        self.attrib_blacklist = attrib_blacklist
        self.remove_javascript = remove_javascript
        self.raise_error = raise_error
        self.suppress = False

    def handle_data(self, data):
        if self.suppress: return
        if data:
            self.result.append(data)

    def handle_charref(self, name):
        if self.suppress: return
        self.result.append('&#%s;' % name)

    def handle_comment(self, comment):
        pass

    def handle_decl(self, data):
        pass

    def handle_entityref(self, name):
        if self.suppress: return
        if self.entitydefs.has_key(name):
            x = ';'
        else:
            # this breaks unstandard entities that end with ';'
            x = ''

        self.result.append('&%s%s' % (name, x))

    def unknown_starttag(self, tag, attrs):
        """ Delete all tags except for legal ones.
        """

        if self.suppress: return
        if self.valid.has_key(tag):
            self.result.append('<' + tag)

            for k, v in attrs:

                ok = False
                if self._isAllowedAttribute(tag, k):
                    ok = True

                    if self.remove_javascript:
                        if k.strip().lower().startswith('on') :
                            ok = False
                            if not self.raise_error: continue
                            else: raise IllegalHTML, 'Javascript event "%s" not allowed.' % k
                        elif v.strip().lower().startswith('javascript:' ):
                            ok = False
                            if not self.raise_error: continue
                            else: raise IllegalHTML, 'Javascript URI "%s" not allowed.' % v

                if ok:
                    self.result.append(' %s="%s"' % (k, v))

                else:
                    print 'removed: ' + tag + ",  " + k + ", " + v


            if self.valid.get(tag):
                self.result.append('>')
            else:
                self.result.append(' />')
        elif self.nasty.has_key(tag):
            self.suppress = True
            if self.raise_error:
                raise IllegalHTML, 'Dynamic tag "%s" not allowed.' % tag
        else:
            # omit tag
            pass

    def unknown_endtag(self, tag):
        print tag
        #FIXME: richtige reihenfolge?, pruefe valid_tag zuerst
        if self.nasty.has_key(tag):
            self.suppress = False
        if self.suppress: return
        if self.valid.get(tag):
            self.result.append('</%s>' % tag)
            #remTag = '</%s>' % tag

    def getResult(self):
        return ''.join(self.result)


    def _isAllowedAttribute(self, tag, attrib):

        #blacklist
        if self.attrib_blacklist.has_key(tag) and (self.attrib_blacklist[tag].has_key(attrib)
                                                   or self.attrib_blacklist[tag].has_key('*')):
            return False
        elif self.attrib_blacklist.has_key('*') and (self.attrib_blacklist['*'].has_key(attrib)
                                                     or self.attrib_blacklist['*'].has_key('*')):
            return False
        else:
            # whitelist
            if self.attrib_whitelist.has_key(tag) and (self.attrib_whitelist[tag].has_key(attrib)
                                                       or self.attrib_whitelist[tag].has_key('*')):
                return True
            elif self.attrib_whitelist.has_key('*') and (self.attrib_whitelist['*'].has_key(attrib)
                                                         or self.attrib_whitelist['*'].has_key('*')):
                return True

        return False


def scrubHTML(html, valid, nasty,
              attrib_whitelist, attrib_blacklist,
              remove_javascript, raise_error=True):

    """ Strip illegal HTML tags from string text.
    """
    parser = StrippingParser(valid=valid, nasty=nasty,
                             attrib_whitelist=attrib_whitelist, attrib_blacklist=attrib_blacklist,
                             remove_javascript=remove_javascript, raise_error=raise_error)
    parser.feed(html)
    parser.close()
    return parser.getResult()

class SafeHTML:
    """Simple transform which uses CMFDefault functions to
    clean potentially bad tags"""

    __implements__ = itransform

    __name__ = "safe_html"
    inputs   = ('text/html',)
    output = "text/x-html-safe"

    # all data used in the parser is kept in private dicts

    _nasty_tags = {}
    _attrib_whitelist = {}
    _attrib_blacklist = {}
    _valid_tags = {}
    _empty = {'base': 1,
              'link': 1,
              'hr': 1,
              'br': 1,
              'param': 1,
              'img': 1,
              'area': 1,
              'input': 1,
              'col': 1,}

    def __init__(self, name=None, **kwargs):


        nasty_tags = []
        for k,v in NASTY_TAGS.items():
            nasty_tags.append(k)


        self.config = {
            'inputs': self.inputs,
            'output': self.output,
            'valid_tags': VALID_TAGS,
            'nasty_tags': tuple(nasty_tags),
            'remove_javascript': 1,
            'attrib_whitelist': {'*': '*',},
            'attrib_blacklist': {'*': 'javascript',},
            'disable_transform': 0,
            }

        self.config_metadata = {
            'inputs'           : ('list', 'Inputs', 'Input(s) MIME type. Change with care.'),
            'valid_tags'       : ('dict', 'valid_full_tags',
                                  'Valid html-tags. Type is 1 for full tags (with a closing '
                                   + 'part (e.g. <p>...</p>)) and 0 for empty tags (e.g. <br />)',
                                  ('tag', 'type')),
            'nasty_tags'       : ('list', 'nasty_tags', 'Dynamic Tags that are striped with '
                                   + 'everything they contain'),
            'remove_javascript': ('int', 'Remove Javascript',
                                  'removes <script type="text/javascript">, '
                                   + '<.. onXXXX=".."> and <a href="javascript...">'),
            'attrib_whitelist' : ('dict', 'Attribute Whitelist',
                                  'All Attributes that are allowed. If empty or *, all Attributes are allowed',
                                  ('Tags','Attributes')),
            'attrib_blacklist' : ('dict', 'Attribute Blacklist',
                                  'Tags/Attributes-Pairs (comma seperated). The given attributes '
                                   + 'are stripped from the given tags, if *, all attributes are removed.',
                                  ('Tags','Attributes')),
            'disable_transform': ('int', 'Disable Transform', ''),
            }

        self.config.update(kwargs)

        if name:
            self.__name__ = name

    def _toList(self, token_string):
        return token_string.replace(',',' ').lower().split()

    def _toString(self, token_list):
        return ", ".join(token_list)
        
    def name(self):
        return self.__name__

    def __getattr__(self, attr):
        if attr == 'inputs':
            return self.config['inputs']
        if attr == 'output':
            return self.config['output']
        raise AttributeError(attr)

    def convert(self, orig, data, **kwargs):

      
        if not self.config['disable_transform']:

            #turn nasty_tags into dict for faster lookup
            for tag in self.config['nasty_tags']:
                self._nasty_tags[tag] = 1

            # build self._valid_tags and make sure that empty_tags are set right
            if self.config['valid_tags']:
                self._valid_tags = self.config['valid_tags']
            else:
                self._valid_tags = {'*': '*'}

            # make sure <script> is removed if remove_javascript is set
            if self.config['remove_javascript']:
                if self._valid_tags.has_key('script'):
                    del self._valid_tags['script']

                self._nasty_tags['script'] = 1
                
                
            
            for key in self._empty.keys():
                if self._valid_tags.has_key(key):
                    self._valid_tags[key] = 0
                

            # build self._attrib_[blacklist|whitelist]
            self._buildFilterDict(self.config['attrib_whitelist'],
                                  self.config['attrib_blacklist'])


            try:
                safe = scrubHTML(bodyfinder(orig),
                                 self.config["valid_tags"],
                                 self._nasty_tags,
                                 self._attrib_whitelist,
                                 self._attrib_blacklist,
                                 self.config["remove_javascript"], raise_error=False)
            except IllegalHTML, inst:
                data.setData(msg_pat % ("Error", str(inst)))
            else:
                data.setData(safe)
       
        else:
            data.setData(orig)

        return data
        
    def _buildFilterDict(self, attrib_whitelist, attrib_blacklist):

        if not attrib_whitelist:
            attrib_whitelist = {'*': '*',}

        self._attrib_whitelist = {}
        for k,v in attrib_whitelist.items():
            tags = self._toList(k)
            attribs = self._toList(v)
            for tag in tags:
                
                if not self._attrib_whitelist.has_key(tag):
                    self._attrib_whitelist[tag] = {}
                for attrib in attribs:
                    self._attrib_whitelist[tag][attrib] = 1

        self._attrib_blacklist = {}
        for k,v in attrib_blacklist.items():
            tags = self._toList(k)
            attribs = self._toList(v)
            for tag in tags:
                if not self._attrib_blacklist.has_key(tag):
                    self._attrib_blacklist[tag] = {}
                for attrib in attribs:
                    self._attrib_blacklist[tag][attrib] = 1
        
    
    
def register():
        
    return SafeHTML()
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.