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

Bloqueo de recursos inseguros en el correo electrónico HTML mediante BeautifulSoup

La documentación de BeautifulSoup afirma que ahorra a los programadores horas o días de trabajo. Esto es un eufemismo.

30 agosto 2021 Actualizado 30 agosto 2021
post main image
https://unsplash.com/@francesphotos

He creado un lector de correo electrónico IMAP utilizando IMAPClient y Flask. El lector de correo electrónico IMAP descodifica el correo electrónico en HTML válido. A continuación, tiene que mostrar este HTML a través del navegador. Funciona bien, hasta ahora.

En este post describo cómo he implementado una opción en mi IMAP E-Mail Reader para bloquear los recursos inseguros en el HTML. Para ello, utilizo BeautifulSoup y las operaciones de expresión regular de Python.

Por qué bloquear los recursos no seguros

Los recursos externos en HTML suelen ser archivos que se incluyen en una página web. Ejemplos son images, stylesheets, bibliotecas de JavaScript. El problema es que te conectan a sistemas remotos. Si usted es consciente de la privacidad que desea evitar esto.

En mi navegador Firefox uso uBlockOrigin, esto no es sólo un bloqueador de anuncios. Del sitio web:

'uBlock Origin es una extensión del navegador gratuita y de código abierto, multiplataforma, para el filtrado de contenidos, cuyo objetivo principal es neutralizar la invasión de la privacidad de una manera eficiente y user amigable'.

La mayoría de los correos electrónicos HTML que recibimos contienen enlaces a recursos externos, a menudo images. Al conectarse a una imagen de este tipo puede ser rastreado. Muchos programas de correo electrónico intentan bloquear estos recursos externos por defecto y ofrecen una opción para permitirlos. El resultado puede ser un correo electrónico de aspecto extraño, la privacidad tiene un precio.

También puede haber correos electrónicos HTML con código añadido intencionadamente, Javascript, para hackear tu ordenador. Hay que eliminar este código.

Por qué BeautifulSoup

Aquí hay algunas formas que podemos utilizar para filtrar el correo electrónico HTML :

  • re: Python's regular expression operations
  • BeautifulSoup: una librería para raspar información de páginas web y modificar el contenido
  • Scrapy: un raspado web framework

Python's re es de muy bajo nivel. Lo uso a menudo pero aquí no parece la mejor opción. Scrapy es un framework y probablemente demasiado. Esto deja a BeautifulSoup.

El rendimiento no es realmente tan importante para mi IMAP E-Mail Reader. No necesito filtrar miles de páginas. El filtrado sólo necesita hacerse cuando se muestra un correo electrónico. En un entorno de alto rendimiento podemos optar por almacenar los correos filtrados.

El parser de BeautifulSoup

Al principio experimenté algunos problemas con el parser (por defecto) 'html.parser'. Funcionaba pero en algunas etiquetas de imagen la url de la imagen no era reemplazada. Por supuesto mi error, TLDR. BeautifulSoup recomienda usar el parser lxml o el parser html5lib.

Como yo quería una solución pura de Python opté por el html5parser, que procesa HTML como lo hace un navegador web. Esto es extremadamente importante, ya que sin BeautifulSoup tardaría siglos en escribir un código que pudiera tratar con HTML (intencionadamente) malo.

La salida de BeautifulSoup

BeautifulSoup es una librería para extraer datos de HTML. Eso está bien pero en nuestro caso de uso primero eliminamos elementos y modificamos elementos y luego mostramos el resultado en un navegador.

BeautifulSoup tiene varias opciones de salida pero siempre modifica al menos algunas cosas. A veces esto puede ser bueno, como añadir las etiquetas que faltan, pero en otros casos esto puede no ser lo que queremos. Un poco como, esperemos y veamos.

Cómo bloquear los recursos inseguros

Lo más importante que debemos hacer

  • Eliminar completamente los recursos inseguros
  • Reemplazar los recursos inseguros
  • Arreglar los recursos inseguros

Eliminar completamente los recursos inseguros

Debemos eliminar siempre todos los Javascript del correo electrónico HTML . También queremos eliminar otros elementos como los enlaces a stylesheets.

Reemplazar los recursos inseguros

Si elimináramos images , el correo electrónico podría estropearse. Para evitarlo, sustituimos images en el correo electrónico por una imagen local, un píxel transparente.

Corregir los recursos defectuosos

Algunos enlaces pueden no contener el atributo:

target="_blank"

También queremos añadir el atributo:

rel="noopener noreferrer"

Esto evita pasar la información de referencia al sitio web de destino.

Pero espera, también hay CSS

En el correo electrónico HTML puede haber estilos CSS que hacen referencia a images, fuentes externas. Primero quise usar el paquete CSSUtils pero este no es muy permisivo. Por ejemplo:

background-image: url ('my_url')

genera una excepción porque hay un espacio entre 'url' y '('. Tampoco pude encontrar otro paquete adecuado así que decidí usar las operaciones de expresión regular de Python.

Lo que quiero es reemplazar el código CSS que contiene 'url()' en la parte del valor. En una página HTML podemos tener:

  • CSS en línea
  • Elementos CSS

Para reducir el código, para el CSS inline eliminamos la propiedad completamente, y para los elementos CSS sustituimos la parte del valor por 'url()'.

El HTMLMailFixer Class

Para filtrar los correos HTML he creado la clase HTMLMailFixer, el código es fácil de entender.

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

Uso:

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

Ejemplo

Este es un ejemplo simple con algo de HTML y 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>
"""

Después de pasarlo al HTMLMailFixer el resultado es:

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>

Obsérvese que BeautifulSoup ha realizado algunos cambios como la adición de las etiquetas que faltan y la conversión a UTF-8.

¿Cómo sabemos que se han realizado todas las sustituciones?

En cuanto a los elementos HTML , no lo sabemos. Si BeautifulSoup falla, nosotros también. Afortunadamente, el analizador de html5lib analiza como un navegador. En cuanto a las propiedades CSS, hice un reemplazo rápido y sucio. Debo mirar más en detalle cómo CSS puede incluir recursos externos.

Resumen

No fue difícil usar BeautifulSoup, es muy potente y me ahorró mucho tiempo. Sería bueno si hubiera algo similar para analizar y modificar (mal) CCS. De todos modos, el resultado final es que los correos electrónicos HTML se filtran y se muestran en mi navegador con recursos inseguros eliminados y bloqueados. Con un botón puedo permitir los recursos externos.

El bloqueo de recursos externos debería ser más flexible. Por ejemplo, siempre queremos bloquear Facebook, Google, pero permitir otros recursos.

No pretendo que la solución presentada aquí sea perfecta, es sólo un comienzo.

Enlaces / créditos

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

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

Leer más

BeautifulSoup

Deje un comentario

Comente de forma anónima o inicie sesión para comentar.

Comentarios

Deje una respuesta.

Responda de forma anónima o inicie sesión para responder.