bigywater.tech
Django · 124

XSS 防护与模板自动转义

Django 模板默认对变量做 HTML 转义,大部分 XSS 就这样挡掉了。这篇不重复概念,只用同一个恶意字符串把「哪些写法安全、哪些写法悄悄关掉了保护」逐个渲染出来。

默认转义
{{ p }} 和 format_html 会转义 < > & 引号
关掉保护的 4 种写法
|safe、{% autoescape off %}、mark_safe 过的变量、mark_safe 拼接
自动转义不拦 javascript:
href="{{ u }}" 里的 javascript:alert(1) 原样保留

自动转义与关掉它的写法

实测 · 本机执行输出
  {{ p }}                                      '&lt;script&gt;alert(1)&lt;/script&gt;'                      含原样的 <script>: False
  {{ p|safe }}                                 '<script>alert(1)</script>'                                  含原样的 <script>: True
  {% autoescape off %}{{ p }}{% endautoescape %} '<script>alert(1)</script>'                                  含原样的 <script>: True
  {{ p }}(变量被 mark_safe 过)                     '<script>alert(1)</script>'                                  含原样的 <script>: True
  format_html("<b>{}</b>", P)                  '<b>&lt;script&gt;alert(1)&lt;/script&gt;</b>'               含原样的 <script>: False
  mark_safe("<b>" + P + "</b>")(拼接)            '<b><script>alert(1)</script></b>'                           含原样的 <script>: True

用户输入一旦经过 mark_safe 或 |safe,后面的模板引擎就认为它是可信的 HTML。mark_safe 只该用在你自己完全控制的字符串上。

属性与 URL

实测 · 本机执行输出
  title="{{ q }}"         → <div title="&quot; onmouseover=&quot;alert(1)">x</div>
  title={{ q }}(没加引号)→ <div title=x onmouseover=alert(1)>x</div> ← 空格没被转义,多出了一个属性
  href="{{ u }}",u = javascript:alert(1) → <a href="javascript:alert(1)">x</a>   (原样保留)
  url_has_allowed_host_and_scheme('javascript:alert(1)'       , allowed_hosts={"example.com"}) → False
  url_has_allowed_host_and_scheme('https://example.com/x'     , allowed_hosts={"example.com"}) → True
  url_has_allowed_host_and_scheme('/local/path'               , allowed_hosts={"example.com"}) → True
  url_has_allowed_host_and_scheme('//evil.example/x'          , allowed_hosts={"example.com"}) → False

自动转义是针对 HTML 文本和带引号属性的。属性值不加引号、URL 里的 javascript: 协议都不在它的保护范围;用户提供的链接要先校验协议与主机再输出。

放进 JavaScript

实测 · 本机执行输出
  escapejs        → <script>var d = \u003C/script\u003E\u003Cscript\u003Ealert(1)\u003C/script\u003E;</script>
  json_script     → <script id="cfg" type="application/json">{"name": "\u003C/script\u003E\u003Cscript\u003Ealert(1)\u003C/script\u003E", "n": 1}</script>
  json_script 输出里含原样的 </script>: False
  只用自动转义放进 JS 字符串 → <script>var name = "&quot;;alert(1);//";</script>  ← 引号被转成 &quot;,但这不是 JS 的转义方式,不要这样写

过滤器的组合

实测 · 本机执行输出
  {{ t|linebreaks }}             → '<p>&lt;script&gt;alert(1)&lt;/script&gt;</p>'       含原样的 <script>: False
  {{ t|urlize }}                 → '&lt;script&gt;alert(1)&lt;/script&gt;'              含原样的 <script>: False
  {{ t|linebreaksbr }}           → '&lt;script&gt;alert(1)&lt;/script&gt;'              含原样的 <script>: False
  {{ t|truncatechars:40 }}       → '&lt;script&gt;alert(1)&lt;/script&gt;'              含原样的 <script>: False
  {{ t|striptags }}              → 'alert(1)'                                           含原样的 <script>: False
  {{ t|safe|linebreaks }}        → '<p><script>alert(1)</script></p>'                   含原样的 <script>: True
  {{ t|safe|truncatechars:40 }}  → '<script>alert(1)</script>'                          含原样的 <script>: True

官方文档对 striptags 有「不能当作安全清洗工具」的提醒,本文没有验证这一点,只演示了它对这个字符串的输出。

分步演示

模板转义
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 / Python 3.14.7 / Django 6.1.1(默认用 SQLite,其它数据库会在文中注明),用 django.test.Client 或真实的本机服务器进程实测;计时用 time.perf_counter,数字每次运行会有小幅抖动。直接用 django.template.Template 渲染字符串,只检查输出文本,没有在浏览器里执行
    • 没有验证:浏览器里脚本是否真的执行、CSP 对内联脚本的拦截(见第 121 篇)、富文本场景下 bleach / nh3 之类的清洗库、DOM 型 XSS、Jinja2 后端
    京ICP备2026067502号-1京公网安备11010502063487号