Eliminar tags HTML que no estén en una lista permitida de una cadena de Python

Tengo una cadena que contiene texto y HTML. Quiero eliminar o deshabilitar algunas tags HTML, como , mientras se permiten otras, para poder mostrarlas en una página web de forma segura. Tengo una lista de tags permitidas, ¿cómo puedo procesar la cadena para eliminar otras tags?

Aquí hay una solución simple usando 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() 

Si también desea eliminar el contenido de las tags no válidas, sustituya tag.extract() por tag.hidden .

También puede considerar el uso de lxml y Tidy .

Utilice lxml.html.clean ! ¡Es muy fácil!

 from lxml.html.clean import clean_html print clean_html(html) 

Supongamos el siguiente html:

 html = '''\         a link another link 

a paragraph

secret EVIL!
of EVIL!
Password:
annoying EVIL! spam spam SPAM! '''

Los resultados…

   
a link another link

a paragraph

secret EVIL!
of EVIL! Password: annoying EVIL! spam spam SPAM!

Puede personalizar los elementos que desea limpiar y todo eso.

Las soluciones anteriores a través de Beautiful Soup no funcionarán. Es posible que puedas hackear algo con Beautiful Soup por encima y más allá de ellos, porque Beautiful Soup brinda acceso al árbol de análisis. En un momento, creo que intentaré resolver el problema adecuadamente, pero es un proyecto de una semana o algo así, y no tengo una semana gratis pronto.

Solo para ser específicos, no solo las excepciones de Beautiful Soup arrojarán algunos errores de análisis que el código anterior no detecta; pero también, hay muchas vulnerabilidades XSS muy reales que no se detectan, como:

 <script> 

Probablemente, lo mejor que puede hacer es eliminar el elemento < como < , para prohibir todo HTML, y luego usar un subconjunto restringido como Markdown para representar correctamente el formato. En particular, también puede retroceder y volver a introducir bits comunes de HTML con una expresión regular. Aquí es cómo se ve el proceso, aproximadamente:

 _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'√', text) text = _endsqrt_.sub(r'', text) return _tcre_.sub('<', text) 

No he probado ese código todavía, por lo que puede haber errores. Pero ves la idea general: tienes que poner en una lista negra todo el HTML en general antes de poner en la lista blanca las cosas correctas.

Esto es lo que uso en mi propio proyecto. Los valores / elementos aceptables provienen de feedparser y BeautifulSoup hace el trabajo.

 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 

Algunas pruebas pequeñas para asegurarse de que esto se comporta correctamente:

 tests = [ #text should work ('

this is text

but this too', '

this is text

but this too'), # make sure we cant exploit removal of tags ('<script> alert("Haha, I hacked your page."); </script>', ''), # try the same trick with attributes, gives an Exception ('
load="alert("Haha, I hacked your page.");">1
', Exception), # no tags should be skipped ('', ''), # leave valid tags but remove bad attributes ('1', '1'), ] 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

Bleach hace mejor con más opciones útiles. Está construido en html5lib y listo para la producción. Consulte la documentación para la función bleack.clean . Su configuración predeterminada escapa tags inseguras como mientras permite tags útiles como .

 import bleach bleach.clean(" example") # '<script>evil</script> example' 

Modifiqué la solución de Bryan con BeautifulSoup para abordar el problema planteado por Chris Drost . Un poco crudo, pero hace el trabajo:

 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 

Editar: Actualizado para soportar atributos válidos.

Yo uso FilterHTML . Es simple y le permite definir una lista blanca bien controlada, eliminar URL e incluso hacer coincidir los valores de los atributos con expresiones regulares o tener funciones de filtrado personalizadas por atributo. Si se usa con cuidado podría ser una solución segura. Aquí hay un ejemplo simplificado del readme:

 import FilterHTML # only allow: #  tags with valid href URLs #  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) 

Podría usar html5lib , que usa una lista blanca para sanear.

Un ejemplo:

 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) 

Prefiero la solución lxml.html.clean , como señala nosklo . Aquí también para eliminar algunas tags vacías:

 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)