在开发Web应用程序时,将用户输入的数据直接显示在HTML页面上是非常危险的。这就像是打开了XSS(跨站脚本攻击)的大门。恶意用户提交包含<script>标签的数据,如果这些数据在其他用户的浏览器中被直接渲染,则可能导致会话Cookie被窃取或恶意代码被执行。

Django提供了一套强大的工具集django.utils.html,旨在根本上阻止这些安全威胁,并安全地处理HTML。 🛡️


1. XSS防御的关键:escape()



这是该模块中最基本和核心的函数。escape()将字符串中的特定HTML特殊字符转换为HTML实体,使浏览器将其识别为普通文本而不是标签。

  • < 转换为 &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,您可能还想完全移除所有标签,只提取纯文本。例如,您想要移除博客正文中的所有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以外的所有参数(argskwargs)进行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为{{ 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的核心安全哲学(自动转义)在Python代码级别实现的必备工具。

  • 要防止XSS,请使用escape()。(在模板中自动)
  • 要删除所有标签,请使用strip_tags()
  • 在Python代码中安全地生成HTML时,务必使用format_html()
  • 当需要将列表数据组合为HTML时,请使用format_html_join()
  • 在将Python数据传递到JavaScript时,json_script()是最安全和标准的方法。
  • mark_safe@html_safe会禁用自动转义,因此只有在确实需要的情况下才应使用,而应优先使用format_html

正确理解和使用这些工具,可以帮助您构建安全坚固的Django应用程序。