Entfernen Sie HTML-Tags, die nicht in einer zulässigen Liste enthalten sind, aus einer Python-Zeichenfolge

72

Ich habe eine Zeichenfolge, die Text und HTML enthält. Ich möchte einige HTML-Tags entfernen oder auf andere Weise deaktivieren, z. B. <script>während andere zugelassen werden, damit ich sie sicher auf einer Webseite rendern kann. Ich habe eine Liste zulässiger Tags. Wie kann ich die Zeichenfolge verarbeiten, um andere Tags zu entfernen?

Everett Toews
quelle
5
es sollte auch alle Attribute entfernen, die nicht auf der Whitelist stehen ... bedenken Sie<img src="heh.png" onload="(function(){/* do bad stuff */}());" />
Dagg Nabbit
.. und auch die nutzlosen leeren Tags und möglicherweise aufeinanderfolgende brTags
Ducu
1
Beachten Sie, dass die ersten beiden Antworten gefährlich sind, da es sehr einfach ist, XSS vor BS / lxml zu verbergen.
fatal_error

Antworten:

44

Hier ist eine einfache Lösung mit 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()

Wenn Sie möchten , als auch, Ersatz , den Inhalt der ungültigen Tags entfernen tag.extract()für tag.hidden.

Sie könnten auch die Verwendung von lxml und Tidy untersuchen .

Bryan
quelle
Danke, ich brauchte diesen Geldautomaten nicht, wusste aber, dass ich in Zukunft so etwas finden musste.
John Farrell
1
Die import-Anweisung sollte wahrscheinlich sein from BeautifulSoup import BeautifulSoup.
Nikhil Chelliah
8
Möglicherweise möchten Sie auch die Verwendung von Attributen einschränken. Fügen Sie dazu einfach die obige Lösung hinzu: valid_attrs = 'href src'.split () für ...: ... tag.attrs = [(attr, val) für attr, val in tag.attrs wenn attr in valid_attrs] hth
Gerald Senarclens de Grancy
9
Das ist nicht sicher! Siehe die Antwort von Chris Dost: stackoverflow.com/questions/699468/…
Thomas
Das ist fantastisch! Eine Sache, um BeautifulSoap 4 zu installieren, laufen: easy_install beautifulsoup4 Dann importieren: von bs4 importieren BeautifulSoup Siehe crummy.com/software/BeautifulSoup/bs4/doc für Details
somecallitblues
60

Verwenden Sie lxml.html.clean! Es ist sehr leicht!

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

Angenommen, das folgende 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>'''

Die Ergebnisse...

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

Sie können die Elemente anpassen, die Sie reinigen möchten und so weiter.

nosklo
quelle
Die lxml.html.clean.clean()Methode finden Sie in der Dokumentzeichenfolge . Es hat viele Möglichkeiten!
Denilson Sá Maia
2
Beachten Sie, dass dies einen Blacklist-Ansatz verwendet, um böse Teile herauszufiltern, anstatt eine Whitelist, aber nur ein Whitelist-Ansatz kann die Sicherheit gewährleisten.
Søren Løvborg
4
@ SørenLøvborg: Der Reiniger unterstützt auch eine Whitelist mit allow_tags.
Martijn Pieters
39

Die oben genannten Lösungen über Beautiful Soup funktionieren nicht. Möglicherweise können Sie mit Beautiful Soup darüber hinaus etwas hacken, da Beautiful Soup den Zugriff auf den Analysebaum ermöglicht. In einer Weile werde ich versuchen, das Problem richtig zu lösen, aber es ist ein einwöchiges Projekt oder so, und ich habe bald keine freie Woche mehr.

Um genau zu sein, wird Beautiful Soup nicht nur Ausnahmen für einige Analysefehler auslösen, die der obige Code nicht abfängt. Es gibt aber auch viele sehr reale XSS-Schwachstellen, die nicht erkannt werden, wie zum Beispiel:

<<script>script> alert("Haha, I hacked your page."); </</script>script>

Wahrscheinlich ist das Beste, was Sie tun können, stattdessen das <Element als &lt;zu entfernen, um den gesamten HTML-Code zu verbieten , und dann eine eingeschränkte Teilmenge wie Markdown zu verwenden, um die Formatierung ordnungsgemäß zu rendern. Insbesondere können Sie auch zurückgehen und allgemeine HTML-Teile mit einem regulären Ausdruck wieder einführen. So sieht der Prozess ungefähr aus:

_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'&radic;<span style="text-decoration:overline;">', text)
    text = _endsqrt_.sub(r'</span>', text)
    return _tcre_.sub('&lt;', text)

Ich habe diesen Code noch nicht getestet, daher kann es zu Fehlern kommen. Aber Sie sehen die allgemeine Idee: Sie müssen den gesamten HTML-Code im Allgemeinen auf die schwarze Liste setzen, bevor Sie die OK-Liste auf die Whitelist setzen.

Alan Moore
quelle
3
Wenn Sie dies zuerst versuchen, tun Sie Folgendes: Importieren von Re aus Markdown Importieren von Markdown Wenn Sie kein Markdown haben, können Sie easy_install
Luke Stanley
25

Folgendes verwende ich in meinem eigenen Projekt. Die akzeptablen_Elemente / Attribute stammen von feedparser und BeautifulSoup erledigt die Arbeit.

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

Einige kleine Tests, um sicherzustellen, dass sich dies korrekt verhält:

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" alt="good">1</div>', '<a href="good" alt="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
Jochen Ritzel
quelle
3
Das ist nicht sicher! Siehe die Antwort von Chris Dost: stackoverflow.com/questions/699468/…
Thomas
1
@Thomas: Haben Sie etwas, um diese Behauptung zu stützen? Chris Dosts "unsicherer" Code löst eigentlich nur eine Ausnahme aus, also haben Sie es wohl nicht wirklich versucht.
Jochen Ritzel
2
@ THC4k: Entschuldigung, ich habe vergessen zu erwähnen, dass ich das Beispiel ändern musste. Hier ist eine, die funktioniert:<<script></script>script> alert("Haha, I hacked your page."); <<script></script>script>
Thomas
Außerdem tag.extract()ändert das eine Liste, über die wir iterieren. Das verwirrt die Schleife und führt dazu, dass das nächste Kind übersprungen wird.
Thomas
@ Thomas: Wirklich schöne Fänge! Ich glaube, ich habe beide Probleme behoben, vielen Dank!
Jochen Ritzel
23

Bleichen macht es besser mit nützlicheren Optionen. Es basiert auf html5lib und ist produktionsbereit. Überprüfen Sie die Dokumentation für die bleack.cleanFunktion. Die Standardkonfiguration entgeht unsicheren Tags wie <script>und ermöglicht nützliche Tags wie <a>.

import bleach
bleach.clean("<script>evil</script> <a href='http://example.com'>example</a>")
# '&lt;script&gt;evil&lt;/script&gt; <a href="http://example.com">example</a>'
Chuangbo
quelle
Erlaubt Bleach weiterhin standardmäßig Daten: URLs über html5lib? Man kann data:zum Beispiel eine URL mit dem Inhaltstyp HTML einbetten .
Antti Haapala
2019 und damit zu kämpfen: stackoverflow.com/questions/7538600/… - Für mich war lxml.html.cleaner solider und entfernte Style-Tags vollständig, während Bleach Sie mit Ihrem CSS als Inhalt sichtbar macht.
Benzkji
11

Ich veränderte Bryan ‚s Lösung mit BeautifulSoup die Adresse Problem von Chris Drost angehoben . Ein bisschen grob, macht aber den Job:

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

Bearbeiten: Aktualisiert, um gültige Attribute zu unterstützen.

Kiran Jonnalagadda
quelle
tag.attrs = [(attr, value) for attr, value in tag.attrs if attr in valid_tags[tag.name]]- tag.attrs ist ein Diktat, daher sollte dies tag.attrs = {attr: value for attr, value in tag.attrs.items() if attr in valid_tags[tag.name]}die bs4
Wee
3

Ich benutze FilterHTML . Es ist einfach und ermöglicht es Ihnen, eine gut kontrollierte Whitelist zu definieren, URLs zu bereinigen und sogar Attributwerte mit Regex abzugleichen oder benutzerdefinierte Filterfunktionen pro Attribut zu verwenden. Bei sorgfältiger Anwendung kann dies eine sichere Lösung sein. Hier ist ein vereinfachtes Beispiel aus der Readme-Datei:

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)
codedvillain
quelle
2

Sie können html5lib verwenden , das zur Bereinigung eine Whitelist verwendet.

Ein Beispiel:

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) 
Brian Neal
quelle
Warum gibt sanitizer_factoryes? Sie sollten HTMLSanitizerdirekt passieren .
Chris Morgan
@ ChrisMorgan gute Frage. Ich glaube, ich habe dieses Beispiel von der html5lib-Site erhalten, und sie haben dem Desinfektionsmittel in der Fabrik etwas angetan, bevor sie es zurückgegeben haben. Aber was sie taten, war in der Entwicklerversion und funktionierte in der veröffentlichten Version nicht. Also habe ich gerade die Leitung entfernt. Hier sieht es komisch aus. Ich werde es recherchieren und möglicherweise die Antwort aktualisieren.
Brian Neal
@ChrisMorgan Es sieht so aus, als hätte die Funktion, auf die ich mich bezog (Token entfernen, anstatt ihnen zu entkommen), es nie stromaufwärts geschafft, also habe ich einfach das Fabrikgeschäft entfernt. Vielen Dank.
Brian Neal
1

Ich ziehe die lxml.html.cleanLösung, wie nosklo Punkte aus . Hier sind auch einige leere Tags zu entfernen:

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)
ducu
quelle
Es ist besser, "return _transform_result (type (clean_html), root)" anstelle von "return tostring (root)" zu verwenden. Es wird die Typprüfung durchführen.
Luckyjazzbo
@luckyjazzbo: Ja, aber dann würde ich eine Methode verwenden, die mit Unterstreichung beginnt. Dies sind Details zur privaten Implementierung und sollten nicht verwendet werden, da sie sich in einer zukünftigen Version von lxml ändern können.
Nosklo
Anscheinend richtig: _transform_result existiert heute (nicht mehr) in lxml.
Simon Steinberger