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()