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

Blocage des ressources non sécurisées dans les courriers électroniques HTML à l'aide de BeautifulSoup

La documentation de BeautifulSoup indique qu'il permet aux programmeurs d'économiser des heures ou des jours de travail. C'est un euphémisme.

30 août 2021 Mise à jour 30 août 2021
post main image
https://unsplash.com/@francesphotos

J'ai créé un lecteur de courrier électronique IMAP en utilisant IMAPClient et Flask. Le lecteur de courrier électronique IMAP décode le courrier électronique en HTML valide. Il doit ensuite afficher cette HTML dans le navigateur. Cela fonctionne bien, jusqu'à présent.

Dans ce billet, je décris comment j'ai implémenté une option dans mon lecteur de courrier électronique IMAP pour bloquer les ressources non sécurisées dans le HTML. Pour ce faire, j'utilise BeautifulSoup et les opérations d'expression régulière de Python.

Pourquoi bloquer les ressources non sécurisées

Les ressources externes dans HTML sont généralement des fichiers qui sont inclus dans une page Web. Les exemples sont images, stylesheets, les bibliothèques JavaScript. Le problème est qu'ils vous connectent à des systèmes distants. Si vous êtes soucieux de votre vie privée, vous voudrez éviter cela.

Dans mon navigateur Firefox, j'utilise uBlockOrigin, qui n'est pas seulement un bloqueur de publicité. Extrait du site Web :

uBlock Origin est une extension de navigateur multiplateforme gratuite et open-source pour le filtrage de contenu, visant principalement à neutraliser l'invasion de la vie privée par une méthode efficace et conviviale user".

La plupart des e-mails HTML que nous recevons contiennent des liens vers des ressources externes, souvent images. En vous connectant à une telle image, vous pouvez être pisté. De nombreux programmes de messagerie essaient de bloquer ces ressources externes par défaut et proposent une option pour les autoriser. Le résultat peut être un e-mail à l'aspect étrange, la vie privée a un prix.

Il peut aussi y avoir des e-mails HTML avec un code ajouté intentionnellement, Javascript, pour pirater votre ordinateur. Nous devons supprimer ce code.

Pourquoi BeautifulSoup

Voici quelques moyens que nous pouvons utiliser pour filtrer les e-mails HTML :

  • re : Les opérations d'expression régulière de Python
  • BeautifulSoup : une bibliothèque pour gratter des informations sur des pages web et en modifier le contenu
  • Scrapy : un outil de scraping web framework

Le re de Python est de très bas niveau. Je l'utilise souvent mais ici il ne semble pas être le meilleur choix. Scrapy est un framework et est probablement surdimensionné. Il reste donc BeautifulSoup.

Les performances ne sont pas vraiment importantes pour mon lecteur de courrier électronique IMAP . Je n'ai pas besoin de filtrer des milliers de pages. Le filtrage ne doit être effectué que lors de l'affichage d'un courriel. Dans un environnement à haute performance, nous pouvons choisir de stocker les emails filtrés.

Le parseur BeautifulSoup

Au début, j'ai rencontré quelques problèmes avec l'analyseur (par défaut) 'html.parser'. Il fonctionnait mais dans quelques tags d'image l'url de l'image n'était pas remplacé. Bien sûr, c'est mon erreur, TLDR. BeautifulSoup vous recommande d'utiliser soit le parseur lxml, soit le parseur html5lib.

Comme je voulais une solution purement Python , j'ai opté pour le parseur html5, qui traite HTML comme le fait un navigateur web. C'est extrêmement important : sans BeautifulSoup, il faudrait des siècles pour écrire un code capable de traiter les mauvaises HTML (intentionnellement).

Le résultat de BeautifulSoup

BeautifulSoup est une bibliothèque permettant d'extraire des données de HTML. C'est bien, mais dans notre cas d'utilisation, nous supprimons et modifions d'abord des éléments, puis nous affichons le résultat dans un navigateur.

BeautifulSoup propose plusieurs options de sortie, mais il modifie toujours au moins quelques éléments. Parfois, cela peut être bon, comme l'ajout de balises manquantes, mais dans d'autres cas, ce n'est pas ce que nous voulons. Un peu comme, attendons et voyons.

Comment bloquer les ressources non sécurisées

Les choses les plus importantes que nous devons faire :

  • Supprimer complètement les ressources non sécurisées
  • Remplacer les ressources dangereuses
  • Réparer les ressources dangereuses

Supprimer complètement les ressources dangereuses

Nous devons toujours supprimer tous les Javascript de l'e-mail HTML . Nous voulons également supprimer d'autres éléments comme les liens vers stylesheets.

Remplacer les ressources non sûres

Si nous supprimons images , l'e-mail peut être endommagé. Pour éviter cela, nous remplaçons images dans l'email par une image locale, un pixel transparent.

Corriger les mauvaises ressources

Certains liens peuvent ne pas contenir l'attribut :

target="_blank"

Nous voulons aussi ajouter l'attribut :

rel="noopener noreferrer"

Cela empêche de transmettre les informations de référence au site Web cible.

Mais attendez, il y a aussi CSS

Dans le courriel HTML , il peut y avoir des styles CSS faisant référence à des images externes, des polices. Je voulais d'abord utiliser le paquet CSSUtils, mais il n'est pas très tolérant. Par exemple :

background-image: url ('my_url')

génère une exception parce qu'il y a un espace entre 'url' et '('. Je n'ai pas non plus trouvé d'autre paquetage approprié et j'ai donc décidé d'utiliser les opérations d'expression régulière de Python.

Ce que je veux, c'est remplacer le code CSS qui contient 'url()' dans la partie valeur. Dans une page HTML nous pouvons avoir :

  • CSS en ligne
  • Éléments CSS

Pour réduire le code, pour les CSS en ligne, nous supprimons complètement la propriété, et pour les éléments CSS, nous remplaçons la partie valeur par 'url()'.

Le correcteur de courrier HTML Class

Pour filtrer les emails HTML j'ai créé la classe HTMLMailFixer, le code est facile à comprendre.

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

Utilisation :

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

Exemple

Ceci est un exemple simple avec quelques HTML et CSS moches :

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

Après l'avoir passé au HTMLMailFixer, le résultat est le suivant :

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>

Notez que BeautifulSoup a effectué quelques modifications comme l'ajout de balises manquantes et la conversion en UTF-8.

Comment savons-nous que tous les remplacements ont été effectués ?

En ce qui concerne les éléments HTML , nous ne le savons pas. Si BeautifulSoup échoue, nous échouons. Heureusement, l'analyseur html5lib analyse comme un navigateur. En ce qui concerne les propriétés CSS, j'ai effectué un remplacement rapide et sale. Je dois examiner plus en détail comment CSS peut inclure des ressources externes.

Résumé

Il n'a pas été difficile d'utiliser BeautifulSoup, il est très puissant et m'a fait gagner beaucoup de temps. Ce serait bien s'il y avait quelque chose de similaire pour analyser et modifier les (mauvais) CCS. Quoi qu'il en soit, le résultat final est le suivant : les e-mails HTML sont filtrés et s'affichent dans mon navigateur avec les ressources dangereuses supprimées et bloquées. Un bouton me permet d'autoriser les ressources externes.

Le blocage des ressources externes devrait être plus souple. Par exemple, nous voulons toujours bloquer Facebook, Google, mais autoriser d'autres ressources.

Je ne prétends pas que la solution présentée ici est parfaite, c'est juste un début.

Liens / crédits

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

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

En savoir plus...

BeautifulSoup

Laissez un commentaire

Commentez anonymement ou connectez-vous pour commenter.

Commentaires

Laissez une réponse

Répondez de manière anonyme ou connectez-vous pour répondre.