在开发Web应用程序时,将用户输入的数据直接显示在HTML页面上是非常危险的。这就像是打开了XSS(跨站脚本攻击)的大门。恶意用户提交包含<script>标签的数据,如果这些数据在其他用户的浏览器中被直接渲染,则可能导致会话Cookie被窃取或恶意代码被执行。
Django提供了一套强大的工具集django.utils.html,旨在根本上阻止这些安全威胁,并安全地处理HTML。 🛡️
1. XSS防御的关键:escape()
这是该模块中最基本和核心的函数。escape()将字符串中的特定HTML特殊字符转换为HTML实体,使浏览器将其识别为普通文本而不是标签。
<转换为<>转换为>'(单引号)转换为'"(双引号)转换为"&转换为&
示例:
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()函数主要在模板外部(例如,在视图逻辑、生成API响应时)需要手动处理HTML时使用。
2. 移除所有HTML标签:strip_tags()
有时,除了转义HTML,您可能还想完全移除所有标签,只提取纯文本。例如,您想要移除博客正文中的所有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为{{ 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模块是使Django的核心安全哲学(自动转义)在Python代码级别实现的必备工具。
- 要防止XSS,请使用
escape()。(在模板中自动) - 要删除所有标签,请使用
strip_tags()。 - 在Python代码中安全地生成HTML时,务必使用
format_html()。 - 当需要将列表数据组合为HTML时,请使用
format_html_join()。 - 在将Python数据传递到JavaScript时,
json_script()是最安全和标准的方法。 mark_safe或@html_safe会禁用自动转义,因此只有在确实需要的情况下才应使用,而应优先使用format_html。
正确理解和使用这些工具,可以帮助您构建安全坚固的Django应用程序。
目前没有评论。