ウェブアプリケーションを開発する際、ユーザーから入力されたデータをHTMLページにそのまま表示するのは非常に危険です。これは XSS (Cross-Site Scripting) 攻撃の扉を大きく開くことになります。悪意のあるユーザーが <script> タグを含むデータを送信し、そのデータが別のユーザーのブラウザでそのままレンダリングされると、セッションクッキーが奪われたり、悪質なコードが実行される可能性があります。

Djangoは、これらのセキュリティ脅威を根本的に排除し、HTMLを安全に処理するための強力なツールセットである django.utils.html を提供しています。 🛡️


1. XSS防御の鍵: escape()



このモジュールの最も基本的かつ重要な関数です。 escape() は、文字列内の特定のHTML特殊文字をHTMLエンティティ(Entity)に変換し、ブラウザがそれをタグではなく一般的なテキストとして認識するようにします。

  • <&lt;
  • >&gt;
  • ' (シングルクォーテーション) は &#39;
  • " (ダブルクォーテーション) は &quot;
  • &&amp;

例:

from django.utils.html import escape

# 悪意のあるユーザー入力
malicious_input = "<script>alert('XSS Attack!');</script>"

# escape処理
safe_output = escape(malicious_input)

print(safe_output)
# 結果:
# &lt;script&gt;alert(&#39;XSS Attack!&#39;);&lt;/script&gt;

このように変換された文字列はブラウザでスクリプトとして実行されず、 <script>alert('XSS Attack!');</script> というテキストがそのまま画面に表示されることになります。

[重要] Djangoテンプレートの自動エスケープ (Autoescaping)

幸い、Djangoテンプレートエンジンは基本的に すべての変数を自動的に escape 処理します。

{{ user_input }}

したがって、 escape() 関数はテンプレートの外部で(例:ビューのロジックやAPI応答を生成する際)HTMLを手動で処理する必要がある場合に主に使用されます。


2. すべてのHTMLタグを削除する: strip_tags()

時にはHTMLをエスケープするだけでなく、すべてのタグを削除して純粋なテキスト(plain text)だけを抽出したいことがあります。例えば、ブログ本文のHTMLタグをすべて削除して検索結果の要約文として使用したいときです。

strip_tags() がその役割を果たします。

例:

from django.utils.html import strip_tags

html_content = "<p>これは <strong>非常に重要な</strong> <em>お知らせ</em>です。</p>"

plain_text = strip_tags(html_content)

print(plain_text)
# 結果:
# これは非常に重要なお知らせです。
# (タグ間の空白もきれいに整理されます)

3. 安全にHTMLを生成する: format_html()



最も強力で重要な関数の一つです。

テンプレートではなくPythonコード(例: views.pymodels.py)で動的にHTMLを生成する必要がある場合があります。例えば、モデルのメソッドが管理ページで特定形式のリンクを返すようにしたいことがあります。

その際、Pythonのf-stringや + 演算子で文字列を組み立てるとXSS攻撃に非常に脆弱になります。

format_html(format_string, *args, **kwargs) は、 format_string を除いたすべての引数(args, kwargs)を自動的に escape() 処理し 文字列に挿入します。そして最終結果を "このHTMLは安全です" とマーク(mark_safe)し、テンプレートでエスケープされずにそのままレンダリングされるようにします。

例: (モデルメソッドで管理ページ用のリンクを生成)

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):
        # [悪い例] f-string: self.titleに <script> があった場合XSS発生
        # return f'<a href="/admin/blog/post/{self.id}/change/">{self.title}</a>'

        # [良い例] format_html 使用
        # self.idとself.titleは自動的にescape処理されます。
        url = f"/admin/blog/post/{self.id}/change/"
        return format_html(
            '<a href="{}">{} (編集)</a>',
            url,
            self.title  # もしtitleが "My<script>..." であっても "&lt;script&gt;" になります
        )

4. テキストフォーマットのヘルパー: linebreaksurlize

これらの関数はテンプレートフィルタ(|linebreaks, |urlize)の原本の関数であり、純粋なテキストをHTMLフォーマットに変換するのに役立ちます。

  • linebreaks(text) : 一般的なテキストの改行文字(\n)をHTMLの <p> タグや <br> タグに変換します。ユーザーが textarea に入力したテキストをフォーマットを維持したまま表示する際に便利です。
  • urlize(text) : テキスト内の http://..., https://..., www... などのURLパターンを見つけ、自動的に <a> タグで囲みます。

例:

from django.utils.html import linebreaks, urlize

raw_text = """こんにちは。
django.utils.htmlをテスト中です。

訪問サイト: https://www.djangoproject.com
"""

# 1. 改行を適用
html_with_breaks = linebreaks(raw_text)
# 結果 (おおよそ):
# <p>こんにちは。<br>django.utils.htmlをテスト中です。</p>
# <p>訪問サイト: https://www.djangoproject.com</p>

# 2. URLリンク適用
html_with_links = urlize(html_with_breaks)
# 結果 (おおよそ):
# ...
# <p>訪問サイト: <a href="https://www.djangoproject.com" rel="nofollow">https://www.djangoproject.com</a></p>

5. 複数項目を安全にHTMLで結合する: format_html_join()

format_html() が単一項目をフォーマットするのに対し、 format_html_join()複数項目(リスト、タプルなど)を反復して 安全にHTMLで結合するために使用されます。

format_html_join(separator, format_string, args_list) という形式で使用します。

  • separator: 各項目を区切るHTML (例: '\n', <br>)
  • format_string: 各項目に適用されるHTMLフォーマット (例: <li>{}</li>)
  • args_list: format_stringに順次代入されるデータリスト

例: (Pythonリストを <ul> タグに変換)

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

options = [
    ('item1', '項目 1'),
    ('item2', '<strong>危険な項目 2</strong>'),
]

# format_stringでの {} はargs_listの各タプル全体を意味します。
# {0} はタプルの最初の要素、 {1} は2番目の要素を意味します。
# '項目 1' と '<strong>...' 部分は自動的にエスケープされます。
list_items = format_html_join(
    '\n',  # 各項目を改行で区切る
    '<li><input type="radio" value="{0}">{1}</li>', # 各項目に適用されるフォーマット
    options  # データリスト
)

# list_itemsは「安全な」HTML部分になります。
final_html = format_html('<ul>\n{}\n</ul>', list_items)

# Djangoテンプレートでfinal_htmlを {{ final_html }} でレンダリングすると...

結果 (HTMLソース):

<ul>
<li><input type="radio" value="item1">項目 1</li>
<li><input type="radio" value="item2">&lt;strong&gt;危険な項目 2&lt;/strong&gt;</li>
</ul>

6. データを安全に/ タグで渡す: json_script()

DjangoテンプレートでPythonデータをJavaScript変数に渡す必要がよくあります。その際、 json_script(data, element_id) を使用すると非常に便利で安全です。

この関数はPythonの辞書やリストをJSON文字列に変換し、それを application/json タイプの <script> タグの中に挿入してくれます。

例: (ビューでデータを渡す)

# 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,
    }
    # user_dataをJSONに変換して <script id="user-data-json"> の中に入れる
    context = {
        'user_data_json': json_script(user_data, 'user-data-json')
    }
    return render(request, 'my_template.html', context)

テンプレート (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>

この方法はデータを手動で var user = {{ user_data }}; のように挿入しようとするときに発生する構文エラーやXSSの脆弱性を完全に防ぎます。


7. [高度] HTMLが安全であることを明示する: mark_safe() / html_safe

時には開発者が意図的にHTMLを生成し、このHTMLが100%安全であると確信し、Djangoの自動エスケープ(autoescape)機能をオフにしたい場合があります。

format_html()json_script() のような関数は内部的にこの処理を自動的に行います。

  • mark_safe(s): 文字列 s に "これは安全なHTMLなのでエスケープしないでください" という '安全ラベル' を付けて返します。この関数自体は 何のエスケープ処理も行いません。 したがって、信頼できないデータに絶対使用してはいけません。

  • @html_safe (デコレーター): モデルのメソッドやカスタムテンプレートタグ関数が返す文字列が安全なHTMLであることを明示するために使用します。 format_html を使用しづらい複雑なロジックでHTMLを生成する際に便利です。

例: (モデルメソッドに適用)

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

    # このメソッドはformat_htmlを使用しているので既に安全です (推奨方法)
    def get_username_display(self):
        return format_html("<strong>{}</strong>", self.user.username)

    # このメソッドは複雑なロジック後に@html_safeで安全であることを示しています (高度な方法)
    @html_safe
    def get_complex_display(self):
        # ... (開発者が安全であると保証する複雑なHTML組み合わせロジック) ...
        html_string = f"<div>{self.user.username}</div><p>{self.bio}</p>"
        # この方法はbioに <script> があった場合XSSに脆弱です。
        # @html_safeは非常に慎重に使用する必要があります。
        return html_string

まとめ

django.utils.html モジュールはDjangoの核心的なセキュリティ哲学(Autoescaping)をPythonコードレベルで実装することができる必須ツールです。

  • XSSを防ぐには escape() を使用します。 (テンプレートでは自動)
  • タグをすべて削除するには strip_tags() を使用します。
  • Pythonコードから安全にHTMLを生成するには必ず format_html() を使用する必要があります。
  • リストデータをHTMLに結合する際には format_html_join() を使用します。
  • PythonデータをJavaScriptに渡す際には json_script() が最も安全で標準的な方法です。
  • mark_safe@html_safe は自動エスケープを無効にするため、本当に必要な場合でなければ format_html の代わりに使用することをお勧めします。

これらのツールを正しく理解し使用することで、セキュリティが堅牢なDjangoアプリケーションを作成することができます。