Lors du développement d'applications web, afficher directement les données saisies par l'utilisateur sur une page HTML est extrêmement risqué. Cela ouvre grandement la porte à des attaques XSS (Cross-Site Scripting). Si un utilisateur malveillant soumet des données contenant la balise <script>, et que ces données sont rendues telles quelles dans le navigateur d'un autre utilisateur, il pourrait voir des cookies de session détournés ou des codes malveillants exécutés.

Django fournit une collection d'outils puissants connue sous le nom de django.utils.html pour bloquer ces menaces de sécurité à la source et traiter les HTML de manière sécurisée. 🛡️


1. La clé de la défense contre les XSS : escape()



C'est la fonction la plus basique et essentielle de ce module. escape() transforme certains caractères spéciaux HTML d'une chaîne en entités HTML, ce qui oblige le navigateur à les reconnaître comme du texte brut plutôt que comme des balises.

  • < devient &lt;
  • > devient &gt;
  • ' (apostrophe) devient &#39;
  • " (guillemet) devient &quot;
  • & devient &amp;

Exemple :

from django.utils.html import escape

# Entrée malveillante de l'utilisateur
malicious_input = "<script>alert('XSS Attack!');</script>"

# Traitement par escape
safe_output = escape(malicious_input)

print(safe_output)
# Résultat :
# &lt;script&gt;alert(&#39;XSS Attack!&#39;);&lt;/script&gt;

La chaîne ainsi transformée ne sera pas interprétée par le navigateur comme un script, mais affichera simplement le texte <script>alert('XSS Attack!');</script>.

[Important] Échappement automatique des modèles Django

Heureusement, le moteur de templates de Django effectue par défaut l'échappement automatique de toutes les variables.

{{ user_input }}

Par conséquent, la fonction escape() est principalement utilisée lorsque vous devez traiter manuellement le HTML en dehors du modèle (par exemple, lors de la logique de vue ou lors de la génération de réponses API).


2. Supprimer toutes les balises HTML : strip_tags()

Parfois, il peut être nécessaire de ne pas seulement échapper des HTML, mais de supprimer complètement toutes les balises et d'extraire uniquement le texte brut. Par exemple, lorsque vous souhaitez enlever toutes les balises HTML du corps d'un blog pour les utiliser comme résumé des résultats de recherche.

strip_tags() remplit précisément ce rôle.

Exemple :

from django.utils.html import strip_tags

html_content = "<p>Ceci est une <strong>information</strong> <em>importante</em>.</p>"

plain_text = strip_tags(html_content)

print(plain_text)
# Résultat :
# Ceci est une information importante.
# (Les espaces entre les balises sont également nettoyés)

3. Générer du HTML en toute sécurité : format_html()



C'est l'une des fonctions les plus puissantes et importantes.

Parfois, vous devez générer dynamiquement du HTML dans du code Python (par exemple, views.py ou models.py). Par exemple, vous pourriez vouloir qu'une méthode de modèle retourne un lien d'un certain format sur la page d'administration.

Assembler des chaînes avec une f-string ou l'opérateur + peut rendre votre code très vulnérable aux attaques XSS.

format_html(format_string, *args, **kwargs) effectue automatiquement un échappement de tous les arguments (excepté format_string) avant de les insérer dans la chaîne. Il indique également que le résultat final est "cet HTML est sécurisé" (mark_safe), de sorte qu'il soit rendu tel quel sans échappement dans le modèle.

Exemple : (Génération de lien pour la page d'administration dans une méthode de modèle)

from django.db import models
from django.utils.html import format_html
from django.utils.text import slugify

class Post(models.Model):
    title = models.CharField(max_length=100)

    def get_edit_link(self):
        # [Mauvaise pratique] f-string : si self.title contient <script>, XSS se produira
        # return f'<a href="/admin/blog/post/{self.id}/change/">{self.title}</a>'

        # [Bonne pratique] utiliser format_html
        # self.id et self.title seront automatiquement échappés.
        url = f"/admin/blog/post/{self.id}/change/"
        return format_html(
            '<a href="{}">{} (modifier)</a>',
            url,
            self.title  # même si title est "Mon<script>...", cela deviendra "&lt;script&gt;"
        )

4. Outils de formatage de texte : linebreaks et urlize

Ces fonctions sont les fonctions de base des filtres de template (|linebreaks, |urlize) et sont utiles pour convertir du texte brut en format HTML.

  • linebreaks(text) : transforme les caractères de retour à la ligne (\n) en balises <p> ou <br> HTML. Cela est utile lorsque vous souhaitez afficher le texte saisi par l'utilisateur dans un textarea tout en conservant le format.
  • urlize(text) : cherche des modèles d'URL tels que http://..., https://..., www... dans le texte et les entoure automatiquement de balises <a>.

Exemple :

from django.utils.html import linebreaks, urlize

raw_text = """Bonjour.
Je teste django.utils.html.

Site visité : https://www.djangoproject.com
"""

# 1. Appliquer des retours à la ligne
html_with_breaks = linebreaks(raw_text)
# Résultat (approximatif) :
# <p>Bonjour.<br>Je teste django.utils.html.</p>
# <p>Site visité : https://www.djangoproject.com</p>

# 2. Appliquer des liens URL
html_with_links = urlize(html_with_breaks)
# Résultat (approximatif) :
# ...
# <p>Site visité : <a href="https://www.djangoproject.com" rel="nofollow">https://www.djangoproject.com</a></p>

5. Combiner plusieurs éléments en toute sécurité en HTML : format_html_join()

Tandis que format_html() formate un seul élément, format_html_join() est utilisé pour parcourir plusieurs éléments (listes, tuples, etc.) et les combiner en toute sécurité en HTML.

Utilisé sous la forme format_html_join(separator, format_string, args_list).

  • separator : HTML qui sépare chaque élément (ex : '\n', <br>)
  • format_string : format HTML à appliquer à chaque élément (ex : <li>{}</li>)
  • args_list : liste de données à substituer séquentiellement au format_string

Exemple : (conversion d'une liste Python en balise <ul>)

from django.utils.html import format_html_join
from django.utils.safestring import mark_safe

options = [
    ('item1', 'Élément 1'),
    ('item2', '<strong>Élément dangereux 2</strong>'),
]

# {} dans format_string représente chaque tuple de args_list.
# {0} représente le premier élément du tuple, {1} représente le deuxième.
# 'Elément 1' et '<strong>...' seront automatiquement échappés.
list_items = format_html_join(
    '\n',  # Séparation des éléments par retour à la ligne
    '<li><input type="radio" value="{0}">{1}</li>', # Format appliqué à chaque élément
    options  # Liste de données
)

# list_items devient des fragments HTML 'sûrs'.
final_html = format_html('<ul>\n{}\n</ul>', list_items)

# Dans un template Django, final_html est rendu avec {{ final_html }}...

Résultat (source HTML) :

<ul>
<li><input type="radio" value="item1">Élément 1</li>
<li><input type="radio" value="item2">&lt;strong&gt;Élément dangereux 2&lt;/strong&gt;</li>
</ul>

6. Transmettre des données en toute sécurité via la balise : json_script()

Il y a souvent des situations où vous devez transmettre des données Python sous forme de variable JavaScript dans un template Django. À ce moment-là, l'utilisation de json_script(data, element_id) est très pratique et sûre.

Cette fonction convertit des dictionnaires ou listes Python en chaînes JSON et les insère dans une balise <script> de type application/json.

Exemple : (passage de données dans une vue)

# views.py
from django.utils.html import json_script

def my_view(request):
    user_data = {
        'id': request.user.id,
        'username': request.user.username,
        'isAdmin': request.user.is_superuser,
    }
    # Insérer user_data en JSON dans <script id="user-data-json">
    context = {
        'user_data_json': json_script(user_data, 'user-data-json')
    }
    return render(request, 'my_template.html', context)

Template (my_template.html) :

{{ user_data_json }}

<script>
    const dataElement = document.getElementById('user-data-json');
    const userData = JSON.parse(dataElement.textContent);

    console.log(userData.username); // "admin"
</script>

Cette méthode élimine complètement les erreurs de syntaxe ou les vulnérabilités XSS qui pourraient survenir si vous tentiez d'insérer des données manuellement comme var user = {{ user_data }}; en raison de caractères tels que " ou '.


7. [Avancé] Indiquer que l'HTML est sûr : mark_safe() / html_safe

Parfois, il se peut qu'un développeur souhaite délibérément générer du HTML et soit sûr à 100% qu'il est sécurisé, et désire désactiver la fonctionnalité d'échappement automatique de Django.

Des fonctions comme format_html() ou json_script() effectuent automatiquement ce traitement en interne.

  • mark_safe(s) : renvoie la chaîne s accompagnée d'une 'étiquette de sécurité' indiquant "ceci est un HTML sûr, ne pas échapper". Cette fonction elle-même ne fait aucun traitement d'échappement. Par conséquent, elle ne doit jamais être utilisée sur des données non fiables.

  • @html_safe (décorateur) : utilisé pour indiquer qu'une chaîne renvoyée par une méthode de modèle ou une fonction de balise de modèle personnalisée est un HTML sûr. C'est utile lorsque vous créez du HTML via une logique complexe où il est délicat d'utiliser format_html.

Exemple : (appliqué à une méthode de modèle)

from django.db import models
from django.utils.html import format_html, html_safe

class UserProfile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    bio = models.TextField()

    # Cette méthode utilise format_html donc elle est déjà sécurisée (méthode recommandée)
    def get_username_display(self):
        return format_html("<strong>{}</strong>", self.user.username)

    # Cette méthode indique qu'elle est sécurisée par @html_safe après une logique complexe (méthode avancée)
    @html_safe
    def get_complex_display(self):
        # ... (logique de combinaison HTML complexe que le développeur juge sécurisée) ...
        html_string = f"<div>{self.user.username}</div><p>{self.bio}</p>"
        # Cette méthode est vulnérable aux XSS si bio contient <script>.
        # @html_safe doit être utilisé avec une grande prudence.
        return html_string

Résumé

Le module django.utils.html est un outil essentiel qui permet de mettre en œuvre la philosophie de sécurité centrale de Django (Echappement Automatique) au niveau du code Python.

  • Pour prévenir les XSS, utilisez escape(). (automatiquement dans le modèle)
  • Pour supprimer toutes les balises, utilisez strip_tags().
  • Pour générer du HTML en toute s sécurité dans du code Python, utilisez absolument format_html().
  • Pour combiner des données de liste en HTML, utilisez format_html_join().
  • Pour passer des données Python à JavaScript, json_script() est la méthode la plus sûre et standard.
  • Utilisez mark_safe ou @html_safe avec précaution, car elles désactivent l'échappement automatique, il est donc préférable d'utiliser format_html à la place sauf si c'est vraiment nécessaire.

En comprenant et en utilisant ces outils correctement, vous pouvez construire des applications Django avec une sécurité robuste.