从 Python 字符串中删除不在允许列表中的 HTML 标记
Posted
技术标签:
【中文标题】从 Python 字符串中删除不在允许列表中的 HTML 标记【英文标题】:Remove HTML tags not on an allowed list from a Python string 【发布时间】:2010-10-16 12:21:35 【问题描述】:我有一个包含文本和 html 的字符串。我想删除或禁用某些 HTML 标记,例如 <script>
,同时允许其他标记,以便我可以安全地将其呈现在网页上。我有一个允许的标签列表,如何处理字符串以删除任何其他标签?
【问题讨论】:
它还应该删除所有未列入白名单的属性...考虑<img src="heh.png" onload="(function()/* do bad stuff */());" />
.. 还有无用的空标签,可能是连续的br
标签
请注意,前两个答案很危险,因为很容易从 BS/lxml 中隐藏 XSS。
【参考方案1】:
这是一个使用BeautifulSoup的简单解决方案:
from bs4 import BeautifulSoup
VALID_TAGS = ['strong', 'em', 'p', 'ul', 'li', 'br']
def sanitize_html(value):
soup = BeautifulSoup(value)
for tag in soup.findAll(True):
if tag.name not in VALID_TAGS:
tag.hidden = True
return soup.renderContents()
如果您还想删除无效标签的内容,请将tag.extract()
替换为tag.hidden
。
您也可以考虑使用lxml 和Tidy。
【讨论】:
谢谢,我不需要这台 ATM,但我知道我将来需要找到这样的东西。 导入语句应该是from BeautifulSoup import BeautifulSoup
。
您可能还想限制属性的使用。为此,只需将其添加到上面的解决方案中:valid_attrs = 'href src'.split() for ...: ... tag.attrs = [(attr, val) for attr, val in tag.attrs if attr在 valid_attrs] hth
这不安全!查看 Chris Dost 的答案:***.com/questions/699468/…
这太棒了!不过有一件事,要安装 BeautifulSoap 4 运行:easy_install beautifulsoup4 然后导入:from bs4 import BeautifulSoup See crummy.com/software/BeautifulSoup/bs4/doc for details【参考方案2】:
上述通过 Beautiful Soup 的解决方案将不起作用。你也许可以用 Beautiful Soup 破解一些东西,因为 Beautiful Soup 提供了对解析树的访问。过一段时间,我想我会尝试妥善解决问题,但这是一个为期一周左右的项目,而且我很快就没有空闲的一周。
具体来说,Beautiful Soup 不仅会针对上述代码未捕获的某些解析错误抛出异常;而且,还有很多非常真实的 XSS 漏洞没有被捕获,例如:
<<script>script> alert("Haha, I hacked your page."); </</script>script>
也许你能做的最好的事情是去掉&lt;
元素作为&lt;
,禁止所有 HTML,然后使用像Markdown这样的受限子集来正确呈现格式.特别是,您还可以返回并使用正则表达式重新引入 HTML 的常见位。大致流程如下:
_lt_ = re.compile('<')
_tc_ = '~(lt)~' # or whatever, so long as markdown doesn't mangle it.
_ok_ = re.compile(_tc_ + '(/?(?:u|b|i|em|strong|sup|sub|p|br|q|blockquote|code))>', re.I)
_sqrt_ = re.compile(_tc_ + 'sqrt>', re.I) #just to give an example of extending
_endsqrt_ = re.compile(_tc_ + '/sqrt>', re.I) #html syntax with your own elements.
_tcre_ = re.compile(_tc_)
def sanitize(text):
text = _lt_.sub(_tc_, text)
text = markdown(text)
text = _ok_.sub(r'<\1>', text)
text = _sqrt_.sub(r'√<span style="text-decoration:overline;">', text)
text = _endsqrt_.sub(r'</span>', text)
return _tcre_.sub('<', text)
我尚未测试该代码,因此可能存在错误。但是您看到了一般的想法:您必须先将所有 HTML 列入黑名单,然后才能将 ok 的内容列入白名单。
【讨论】:
如果你先尝试这样做: import re from markdown import markdown 如果你没有markdown,你可以尝试easy_install【参考方案3】:这是我在自己的项目中使用的。可接受的元素/属性来自feedparser,BeautifulSoup 完成了这项工作。
from BeautifulSoup import BeautifulSoup
acceptable_elements = ['a', 'abbr', 'acronym', 'address', 'area', 'b', 'big',
'blockquote', 'br', 'button', 'caption', 'center', 'cite', 'code', 'col',
'colgroup', 'dd', 'del', 'dfn', 'dir', 'div', 'dl', 'dt', 'em',
'font', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'img',
'ins', 'kbd', 'label', 'legend', 'li', 'map', 'menu', 'ol',
'p', 'pre', 'q', 's', 'samp', 'small', 'span', 'strike',
'strong', 'sub', 'sup', 'table', 'tbody', 'td', 'tfoot', 'th',
'thead', 'tr', 'tt', 'u', 'ul', 'var']
acceptable_attributes = ['abbr', 'accept', 'accept-charset', 'accesskey',
'action', 'align', 'alt', 'axis', 'border', 'cellpadding', 'cellspacing',
'char', 'charoff', 'charset', 'checked', 'cite', 'clear', 'cols',
'colspan', 'color', 'compact', 'coords', 'datetime', 'dir',
'enctype', 'for', 'headers', 'height', 'href', 'hreflang', 'hspace',
'id', 'ismap', 'label', 'lang', 'longdesc', 'maxlength', 'method',
'multiple', 'name', 'nohref', 'noshade', 'nowrap', 'prompt',
'rel', 'rev', 'rows', 'rowspan', 'rules', 'scope', 'shape', 'size',
'span', 'src', 'start', 'summary', 'tabindex', 'target', 'title', 'type',
'usemap', 'valign', 'value', 'vspace', 'width']
def clean_html( fragment ):
while True:
soup = BeautifulSoup( fragment )
removed = False
for tag in soup.findAll(True): # find all tags
if tag.name not in acceptable_elements:
tag.extract() # remove the bad ones
removed = True
else: # it might have bad attributes
# a better way to get all attributes?
for attr in tag._getAttrMap().keys():
if attr not in acceptable_attributes:
del tag[attr]
# turn it back to html
fragment = unicode(soup)
if removed:
# we removed tags and tricky can could exploit that!
# we need to reparse the html until it stops changing
continue # next round
return fragment
一些小测试以确保其行为正确:
tests = [ #text should work
('<p>this is text</p>but this too', '<p>this is text</p>but this too'),
# make sure we cant exploit removal of tags
('<<script></script>script> alert("Haha, I hacked your page."); <<script></script>/script>', ''),
# try the same trick with attributes, gives an Exception
('<div on<script></script>load="alert("Haha, I hacked your page.");">1</div>', Exception),
# no tags should be skipped
('<script>bad</script><script>bad</script><script>bad</script>', ''),
# leave valid tags but remove bad attributes
('<a href="good" onload="bad" onclick="bad" >1</div>', '<a href="good" >1</a>'),
]
for text, out in tests:
try:
res = clean_html(text)
assert res == out, "%s => %s != %s" % (text, res, out)
except out, e:
assert isinstance(e, out), "Wrong exception %r" % e
【讨论】:
这不安全!查看 Chris Dost 的回答:***.com/questions/699468/… @Thomas:你有什么可以支持这种说法的吗? Chris Dost “不安全”的代码实际上只是引发了一个异常,所以我猜你实际上并没有尝试过。 @THC4k:对不起,我忘了说我必须修改这个例子。这是一个有效的方法:<<script></script>script> alert("Haha, I hacked your page."); <<script></script>script>
另外,tag.extract()
修改了我们正在迭代的列表。这会混淆循环,并导致它跳过下一个孩子。
@Thomas:抓得真好!我想我已经解决了这两个问题,非常感谢!【参考方案4】:
使用lxml.html.clean
!这很容易!
from lxml.html.clean import clean_html
print clean_html(html)
假设以下html:
html = '''\
<html>
<head>
<script type="text/javascript" src="evil-site"></script>
<link rel="alternate" type="text/rss" src="evil-rss">
<style>
body background-image: url(javascript:do_evil);
div color: expression(evil);
</style>
</head>
<body onload="evil_function()">
<!-- I am interpreted for EVIL! -->
<a href="javascript:evil_function()">a link</a>
<a href="#" onclick="evil_function()">another link</a>
<p onclick="evil_function()">a paragraph</p>
<div style="display: none">secret EVIL!</div>
<object> of EVIL! </object>
<iframe src="evil-site"></iframe>
<form action="evil-site">
Password: <input type="password" name="password">
</form>
<blink>annoying EVIL!</blink>
<a href="evil-site">spam spam SPAM!</a>
<image src="evil!">
</body>
</html>'''
结果...
<html>
<body>
<div>
<style>/* deleted */</style>
<a href="">a link</a>
<a href="#">another link</a>
<p>a paragraph</p>
<div>secret EVIL!</div>
of EVIL!
Password:
annoying EVIL!
<a href="evil-site">spam spam SPAM!</a>
<img src="evil!">
</div>
</body>
</html>
您可以自定义要清理的元素等等。
【讨论】:
查看lxml.html.clean.clean()
方法的文档字符串。它有很多选择!
注意这里使用的是黑名单的方式来过滤掉恶意比特,而不是白名单,但是只有白名单的方式才能保证安全。
@SørenLøvborg:清洁器还支持白名单,使用allow_tags
。
太好了!我喜欢默认配置,但是假设我想添加删除所有<span>
s,我该怎么做?【参考方案5】:
我更喜欢lxml.html.clean
解决方案,例如nosklo points out。这里还要去掉一些空标签:
from lxml import etree
from lxml.html import clean, fromstring, tostring
remove_attrs = ['class']
remove_tags = ['table', 'tr', 'td']
nonempty_tags = ['a', 'p', 'span', 'div']
cleaner = clean.Cleaner(remove_tags=remove_tags)
def squeaky_clean(html):
clean_html = cleaner.clean_html(html)
# now remove the useless empty tags
root = fromstring(clean_html)
context = etree.iterwalk(root) # just the end tag event
for action, elem in context:
clean_text = elem.text and elem.text.strip(' \t\r\n')
if elem.tag in nonempty_tags and \
not (len(elem) or clean_text): # no children nor text
elem.getparent().remove(elem)
continue
elem.text = clean_text # if you want
# and if you also wanna remove some attrs:
for badattr in remove_attrs:
if elem.attrib.has_key(badattr):
del elem.attrib[badattr]
return tostring(root)
【讨论】:
最好使用“return _transform_result(type(clean_html), root)”而不是“return tostring(root)”。它将处理类型检查。 @luckyjazzbo:是的,但是我会使用以下划线开头的方法。这些是私有实现细节,不应使用,因为它们可能会在未来版本的 lxml 中发生变化。 显然正确:_transform_result 今天在 lxml 中不存在(不再存在)。【参考方案6】:我修改了Bryan 的solution with BeautifulSoup 以寻址problem raised by Chris Drost。有点粗略,但确实有效:
from BeautifulSoup import BeautifulSoup, Comment
VALID_TAGS = 'strong': [],
'em': [],
'p': [],
'ol': [],
'ul': [],
'li': [],
'br': [],
'a': ['href', 'title']
def sanitize_html(value, valid_tags=VALID_TAGS):
soup = BeautifulSoup(value)
comments = soup.findAll(text=lambda text:isinstance(text, Comment))
[comment.extract() for comment in comments]
# Some markup can be crafted to slip through BeautifulSoup's parser, so
# we run this repeatedly until it generates the same output twice.
newoutput = soup.renderContents()
while 1:
oldoutput = newoutput
soup = BeautifulSoup(newoutput)
for tag in soup.findAll(True):
if tag.name not in valid_tags:
tag.hidden = True
else:
tag.attrs = [(attr, value) for attr, value in tag.attrs if attr in valid_tags[tag.name]]
newoutput = soup.renderContents()
if oldoutput == newoutput:
break
return newoutput
编辑:已更新以支持有效属性。
【讨论】:
tag.attrs = [(attr, value) for attr, value in tag.attrs if attr in valid_tags[tag.name]]
-- tag.attrs 是一个字典,所以这应该是 tag.attrs = attr: value for attr, value in tag.attrs.items() if attr in valid_tags[tag.name]
使用 bs4【参考方案7】:
我使用FilterHTML。它很简单,可以让您定义一个控制良好的白名单,清理 URL,甚至将属性值与正则表达式匹配,或者为每个属性提供自定义过滤功能。如果小心使用,它可能是一个安全的解决方案。这是自述文件中的一个简化示例:
import FilterHTML # only allow: # <a> tags with valid href URLs # <img> tags with valid src URLs and measurements whitelist = 'a': 'href': 'url', 'target': [ '_blank', '_self' ], 'class': [ 'button' ] , 'img': 'src': 'url', 'width': 'measurement', 'height': 'measurement' , filtered_html = FilterHTML.filter_html(unfiltered_html, whitelist)
【讨论】:
【参考方案8】:您可以使用html5lib,它使用白名单进行清理。
一个例子:
import html5lib
from html5lib import sanitizer, treebuilders, treewalkers, serializer
def clean_html(buf):
"""Cleans HTML of dangerous tags and content."""
buf = buf.strip()
if not buf:
return buf
p = html5lib.HTMLParser(tree=treebuilders.getTreeBuilder("dom"),
tokenizer=sanitizer.HTMLSanitizer)
dom_tree = p.parseFragment(buf)
walker = treewalkers.getTreeWalker("dom")
stream = walker(dom_tree)
s = serializer.htmlserializer.HTMLSerializer(
omit_optional_tags=False,
quote_attr_values=True)
return s.render(stream)
【讨论】:
为什么sanitizer_factory
存在?你应该直接传递HTMLSanitizer
。
@ChrisMorgan 好问题。我想我是从 html5lib 站点获得的这个示例,他们在返回之前对工厂的消毒剂做了一些处理。但他们所做的是在开发版本中,在发布版本中不起作用。所以我只是删除了这条线。这里看起来确实很奇怪。我会研究它并可能更新答案。
@ChrisMorgan 看起来我所指的功能(剥离令牌而不是转义它们)从未在上游实现,所以我只是删除了工厂业务。谢谢。【参考方案9】:
Bleach 使用更多有用的选项会做得更好。它建立在 html5lib 之上,可用于生产。检查bleach.clean
函数的文档。它的默认配置会转义像<script>
这样的不安全标签,同时允许像<a>
这样的有用标签。
import bleach
bleach.clean("<script>evil</script> <a href='http://example.com'>example</a>")
# '<script>evil</script> <a href="http://example.com">example</a>'
【讨论】:
默认情况下,bleach 是否仍然允许 data: urls via html5lib?例如,可以嵌入内容类型为 html 的data:
url。
2019,并为此苦苦挣扎:***.com/questions/7538600/… - 对我来说,lxml.html.cleaner 更可靠,完全删除样式标签,而漂白剂让你的 css 作为内容可见。以上是关于从 Python 字符串中删除不在允许列表中的 HTML 标记的主要内容,如果未能解决你的问题,请参考以下文章