angle-uparrow-clockwisearrow-counterclockwisearrow-down-uparrow-leftatcalendarcard-listchatcheckenvelopefolderhouseinfo-circlepencilpeoplepersonperson-fillperson-plusphoneplusquestion-circlesearchtagtrashx

Blockieren unsicherer Ressourcen in HTML-E-Mails mit BeautifulSoup

In der Dokumentation von BeautifulSoup heißt es, dass es Programmierern Stunden oder Tage an Arbeit erspart. Das ist eine Untertreibung.

30 August 2021 Aktualisiert 30 August 2021
post main image
https://unsplash.com/@francesphotos

Ich habe einen IMAP E-Mail Reader mit IMAPClient und Flask erstellt. Der IMAP E-Mail Reader dekodiert die E-Mail in gültige HTML. Dann muss er diese HTML über den Browser anzeigen. Funktioniert gut, so weit so gut.

In diesem Beitrag beschreibe ich, wie ich eine Option in meinem IMAP E-Mail Reader implementiert habe, um unsichere Ressourcen in der HTML zu blockieren. Dazu verwende ich BeautifulSoup und Pythons Operationen mit regulären Ausdrücken.

Warum unsichere Ressourcen blockieren

Externe Ressourcen in HTML sind in der Regel Dateien, die in einer Webseite enthalten sind. Beispiele sind images, stylesheets, JavaScript-Bibliotheken. Das Problem ist, dass sie eine Verbindung zu entfernten Systemen herstellen. Wenn Sie auf Ihre Privatsphäre achten, sollten Sie dies vermeiden.

In meinem Firefox-Browser verwende ich uBlockOrigin, das ist nicht nur ein Werbeblocker. Von der Website:

'uBlock Origin ist eine kostenlose und quelloffene, plattformübergreifende Browsererweiterung zur Filterung von Inhalten, die in erster Linie darauf abzielt, Eingriffe in die Privatsphäre auf effiziente, user-freundliche Weise zu neutralisieren.'

Die meisten der HTML -E-Mails, die wir erhalten, enthalten Links zu externen Ressourcen, häufig images. Wenn Sie eine Verbindung zu einem solchen Bild herstellen, können Sie nachverfolgt werden. Viele E-Mail-Programme versuchen, diese externen Ressourcen standardmäßig zu blockieren und bieten eine Option an, sie zuzulassen. Das Ergebnis kann eine seltsam aussehende E-Mail sein, die Privatsphäre hat ihren Preis.

Es kann auch vorkommen, dass HTML -E-Mails mit absichtlich eingefügtem Code, Javascript, Ihren Computer hacken. Wir müssen diesen Code entfernen.

Warum BeautifulSoup

Hier sind einige Möglichkeiten, wie wir HTML -E-Mails filtern können:

  • re: Python's reguläre Ausdrucksoperationen
  • BeautifulSoup: eine Bibliothek zum Auslesen von Informationen aus Webseiten und zum Ändern von Inhalten
  • Scrapy: ein Web Scraping framework

Python ist ein sehr einfaches Programm. Ich benutze es oft, aber hier scheint es nicht die beste Wahl zu sein. Scrapy ist ein framework und wahrscheinlich Overkill. Damit bleibt BeautifulSoup.

Leistung ist für meinen IMAP E-Mail Reader nicht wirklich wichtig. Ich brauche nicht Tausende von Seiten zu filtern. Die Filterung muss nur erfolgen, wenn eine E-Mail angezeigt wird. In einer Hochleistungsumgebung können wir uns dafür entscheiden, die gefilterten E-Mails zu speichern.

Der BeautifulSoup-Parser

Am Anfang hatte ich einige Probleme mit dem (Standard-) 'html.parser'. Er funktionierte, aber in einigen Bild-Tags wurde die Bild-Url nicht ersetzt. Das war natürlich mein Fehler, TLDR. BeautifulSoup empfiehlt, entweder den lxml-Parser oder den html5lib-Parser zu verwenden.

Da ich eine reine Python -Lösung wollte, habe ich mich für den html5parser entschieden, der HTML so verarbeitet, wie es ein Webbrowser tut. Das ist extrem wichtig, denn ohne BeautifulSoup würde es ewig dauern, einen Code zu schreiben, der mit (absichtlich) schlechten HTML umgehen kann.

Die Ausgabe von BeautifulSoup

BeautifulSoup ist eine Bibliothek zum Auslesen von Daten aus HTML. Das ist schön, aber in unserem Anwendungsfall entfernen wir zuerst Elemente und ändern Elemente und zeigen dann das Ergebnis in einem Browser an.

BeautifulSoup hat mehrere Ausgabeoptionen, aber es ändert immer mindestens ein paar Dinge. Manchmal kann das gut sein, wie das Hinzufügen fehlender Tags, aber in anderen Fällen ist das vielleicht nicht das, was wir wollen. Ein bisschen wie: Abwarten und sehen.

Wie man unsichere Ressourcen blockiert

Das Wichtigste, was wir tun müssen:

  • Unsichere Ressourcen vollständig entfernen
  • Ersetzen unsicherer Ressourcen
  • Schlechte Ressourcen reparieren

Vollständige Entfernung unsicherer Ressourcen

Wir müssen immer alle Javascript aus der E-Mail HTML entfernen. Wir wollen auch andere Elemente wie Links zu stylesheets entfernen.

Ersetzen unsicherer Ressourcen

Wenn wir images entfernen würden, könnte die E-Mail durcheinander geraten. Um dies zu verhindern, ersetzen wir images in der E-Mail durch ein lokales Bild, ein transparentes Pixel.

Fehlerhafte Ressourcen korrigieren

Einige Links enthalten möglicherweise nicht das Attribut:

target="_blank"

Wir wollen das Attribut ebenfalls hinzufügen:

rel="noopener noreferrer"

Dies verhindert die Weitergabe von Referrer-Informationen an die Ziel-Website.

Aber halt, es gibt auch CSS

In der HTML -E-Mail kann es CSS-Stile geben, die auf externe images-Schriften verweisen. Zuerst wollte ich das Paket CSSUtils verwenden, aber das ist nicht sehr fehlerverzeihend. Zum Beispiel:

background-image: url ('my_url')

Es erzeugt eine Ausnahme, weil zwischen 'url' und '(' ein Leerzeichen steht. Ich konnte auch kein anderes geeignetes Paket finden, also entschied ich mich, die regulären Ausdrucksoperationen von Python zu verwenden.

Was ich möchte, ist CSS-Code zu ersetzen, der 'url()' im Wertteil enthält. In einer HTML Seite können wir haben:

  • Inline-CSS
  • CSS-Elemente

Um den Code zu reduzieren, entfernen wir bei Inline-CSS die Eigenschaft vollständig, und bei CSS-Elementen ersetzen wir den Wertteil durch "url()".

Der HTMLMailFixer Class

Um HTML -E-Mails zu filtern, habe ich die Klasse HTMLMailFixer erstellt, der Code ist einfach zu verstehen.

# html_mail_fixer.py

from bs4 import BeautifulSoup
import re

class HTMLMailFixer:
    
    def __init__(
        self,
        parser='html5lib',
    ):
        self.__parser = parser
        self.forbidden = [
            'script', 'object', 'iframe',
        ]
        self.__allow_remote_resources = None
        self.__block_img_url = None
        self.__soup = None

    def __remove_forbidden_elems(self):
        for elem in self.__soup():
            if elem.name in self.forbidden:
                # remove
                elem.extract()

    def __fix_a_elems(self):
        for a_elem in self.__soup.find_all('a'):
            # add/replace
            a_elem['target'] = '_blank'
            a_elem['rel'] = 'noopener noreferrer'

    def __remove_link_elems(self):
        for link_elem in self.__soup.find_all('link'):
            link_href = link_elem.get('href')
            if link_href is None:
                continue
            # remove
            link_elem.extract()

    def __fix_img_elems(self):
        for img_elem in self.__soup.find_all('img'):
            img_src = img_elem.get('src')
            if img_src is None:
                continue
            # replace
            img_elem['src'] = self.__block_img_url

    def __fix_style_elems(self):
        """
        objective: remove any property with a value starting with 'url('
        actual: replace value 'url(url)' by 'url()'
        """
        match_url_start_pattern = re.compile(r':\s*url\s*\(', re.I)
        for style_elem in self.__soup.find_all('style'):
            # 'contents': [
            #    '\n.section-block {\n    padding: 1em;\n    background-image: url(https://whatever_image_1);\n}\n#logo {\n    margin-top: 10em;\n    background-image: url(https://whatever_image_1);\n}\n'
            # ]
            new_contents = []
            for content in style_elem.contents:
                chunks = re.split(match_url_start_pattern, content) 
                for i, chunk in enumerate(chunks):
                    if i == 0:
                        continue
                    chunks[i] = re.sub(r'.*?\)', ')', chunk)
                # reconstruct
                new_content = ': url(\'\''.join(chunks)
                new_contents.append(new_content)

            # replace
            style_elem.string.replace_with('\n'.join(new_contents))

    def __fix_inline_style(self):
        """
        search elems with style attribute.
        remove any property name:value having a value starting with 'url('
        """
        match_url_start_pattern = re.compile(r'^url\s*\(', re.I)
        for elem in self.__soup.find_all(attrs={'style': True}):
            style_attr = elem['style']
            new_props = []
            props = style_attr
            if ';' in style_attr:
                props = style_attr.split(';')
            for i, prop in enumerate(props):
                if ':' not in prop:
                    # malformed, skip
                    continue
                name, value = prop.split(':', 1)
                if isinstance(value, list):
                    # malformed, skip
                    continue
                value = value.strip()
                if re.match(match_url_start_pattern, value):
                    # found value starting with 'url(' so skip
                    continue
                new_props.append(name + ': ' + value)

            # replace
            elem['style'] = '; '.join(new_props)

    def fix_all(
        self,
        html=None,
        allow_remote_resources=False,
        block_img_url=None,
    ):
        self.__allow_remote_resources = allow_remote_resources
        self.__block_img_url = block_img_url

        # start soup
        self.__soup = BeautifulSoup(html, self.__parser)

        # remove, modify html elements
        self.__remove_forbidden_elems()
        self.__fix_a_elems()
        if not self.__allow_remote_resources:
            self.__remove_link_elems()
            if self.__block_img_url is not None:
                self.__fix_img_elems()
            self.__fix_inline_style()
            self.__fix_style_elems()
        # output
        output = str(self.__soup)
        return re.sub(r'\n\n+', '\n\n', output)

Verwendung:

from .htm_mail_fixer import HTMLMailFixer

html = 'YOUR_HTML'
allow_remote_resources = False
block_img_url = 'YOUR_PIXEL_URL'

html_mail_fixer = HTMLMailFixer()

html_fixed = html_mail_fixer.fix_all(
    html=html,
    allow_remote_resources=allow_remote_resources,
    block_img_url=block_img_url,
)

Beispiel

Dies ist ein einfaches Beispiel mit einigen hässlichen HTML und CSS:

html = """<!DOCTYPE html PUBLIC "- / /w3c / /dtd html 4.01 transitional / /en">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/css/bootstrap.min.css" rel="stylesheet"><style>
.section-block{background-image: url ( ' https://whatever_image_1') !important;
font-size: 1.1em}
#logo {
   margin-top: 10em;background-image: url ( https://whatever_image_1 )}
.wino { color: #ff0000 }
</style>
</head><body style="font: inherit; font-size: 100%; margin:0; padding:0; background-image: url( ' https://whatever_image_2' )">

<a href="https://whatever_a_href_1"><img src="https://whatever_img_src_1" width="60"></a>
<p>Amount: &euro; 1,50</p>
<a href=https://whatever_a_href_2 target=_top><img src=https://whatever_img_src_2 width="60"></a>
<script> a = 'b' </script>
"""

Nachdem es an den HTMLMailFixer übergeben wurde, ist das Ergebnis:

html_fixed = <!DOCTYPE html PUBLIC "- / /w3c / /dtd html 4.01 transitional / /en">
<html><head><style>
.section-block{background-image: url('') !important;
font-size: 1.1em}
#logo {
   margin-top: 10em;background-image: url('')}
.wino { color: #ff0000 }
</style>
</head><body style="font: inherit;  font-size: 100%;  margin: 0;  padding: 0">

<a href="https://whatever_a_href_1" rel="noopener noreferrer" target="_blank"><img src="YOUR_PIXEL_URL" width="60"/></a>
<p>Amount: € 1,50</p>
<a href="https://whatever_a_href_2" rel="noopener noreferrer" target="_blank"><img src="YOUR_PIXEL_URL" width="60"/></a>

</body></html>

Beachten Sie, dass BeautifulSoup einige Änderungen vorgenommen hat, wie das Hinzufügen fehlender Tags und die Konvertierung in UTF-8.

Woher wissen wir, dass alle Ersetzungen vorgenommen wurden?

Bei den HTML -Elementen wissen wir das nicht. Wenn BeautifulSoup versagt, versagen auch wir. Glücklicherweise parst der html5lib-Parser wie ein Browser. Was die CSS-Eigenschaften betrifft, so habe ich eine schnelle und schmutzige Ersetzung vorgenommen. Ich muss mir noch genauer ansehen, wie CSS externe Ressourcen einbeziehen kann.

Zusammenfassung

Es war nicht schwer, BeautifulSoup zu benutzen, es ist sehr leistungsfähig und hat mir viel Zeit gespart. Es wäre schön, wenn es etwas Ähnliches für das Parsen und Ändern von (schlechten) CCS gäbe. Wie auch immer, das Endergebnis ist, dass HTML -E-Mails gefiltert werden und in meinem Browser angezeigt werden, wobei unsichere Ressourcen entfernt und blockiert werden. Mit einer Schaltfläche kann ich externe Ressourcen zulassen.

Das Blockieren von externen Ressourcen sollte flexibler sein. Zum Beispiel wollen wir immer Facebook, Google blockieren, aber andere Ressourcen zulassen.

Ich behaupte nicht, dass die hier vorgestellte Lösung perfekt ist, sie ist nur ein Anfang.

Links / Impressum

Beautiful Soup Documentation
https://www.crummy.com/software/BeautifulSoup/bs4/doc/

HTML <link> Tag
https://www.w3schools.com/Tags/tag_link.asp

Mehr erfahren

BeautifulSoup

Einen Kommentar hinterlassen

Kommentieren Sie anonym oder melden Sie sich zum Kommentieren an.

Kommentare

Eine Antwort hinterlassen

Antworten Sie anonym oder melden Sie sich an, um zu antworten.