在開發網頁應用程式時,直接將用戶輸入的數據顯示在HTML頁面上是非常危險的。這就像是大開了XSS (跨網站腳本攻擊)的大門。惡意用戶提交含有<script>標籤的數據,如果這些數據在其他用戶的瀏覽器中無所遁形地渲染出來,則會導致會話Cookie被盜取或執行惡意代碼。

Django提供了一個強大的工具集,即django.utils.html,來原則性地封鎖這些安全威脅並安全地處理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()函數主要用於需要手動處理HTML的情況(例如:在視圖邏輯、API應答生成時)。


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}代表第二個元素。
# '項目 1'和'<strong>...'的部分將自動進行escape處理。
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 }}將會渲染...

結果(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模塊是一個必不可少的工具,能讓開發者在Python代碼層面實現Django的核心安全哲學(自動轉義)。

  • 為了抵禦XSS,請使用escape()。(在模板中自動)
  • 要刪除所有標籤,請使用strip_tags()
  • 在Python代碼中安全地生成HTML時,務必使用format_html()
  • 當將列表數據合併為HTML時,請使用format_html_join()
  • 將Python數據傳遞到JavaScript時,json_script()是最安全且標準的方法。
  • mark_safe@html_safe會使自動轉義失效,因此只在確實需要時使用,而非用format_html來替代。

正確理解和使用這些工具,將能打造出安全穩固的Django應用程式。