在開發網頁應用程式時,直接將用戶輸入的數據顯示在HTML頁面上是非常危險的。這就像是大開了XSS (跨網站腳本攻擊)的大門。惡意用戶提交含有<script>標籤的數據,如果這些數據在其他用戶的瀏覽器中無所遁形地渲染出來,則會導致會話Cookie被盜取或執行惡意代碼。
Django提供了一個強大的工具集,即django.utils.html,來原則性地封鎖這些安全威脅並安全地處理HTML。🛡️
1. XSS防禦的核心:escape()
該模塊中最基本也是最重要的函數。escape()將字符串中的特定HTML特殊字符轉換為HTML實體(Entity),從而使瀏覽器將其識別為普通文本,而非標籤。
<變為<>變為>'(單引號) 變為'"(雙引號) 變為"&變為&
示例:
from django.utils.html import escape
# 惡意用戶輸入
malicious_input = "<script>alert('XSS Attack!');</script>"
# escape處理
safe_output = escape(malicious_input)
print(safe_output)
# 結果:
# <script>alert('XSS Attack!');</script>
這樣轉換後的字符串不會在瀏覽器中作為腳本執行,而是會顯示<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.py或models.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>..."則會變為"<script>"
)
4. 文本格式助手:linebreaks 和 urlize
這些函數是模板過濾器(|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"><strong>危險項目 2</strong></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應用程式。
目前沒有評論。