vlambda博客
学习文章列表

在Django模板中安全地包含JavaScript数据

Django模板通常用于将数据传递给JavaScript代码。不幸的是,如果实现的不正确的话,就可能会增加HTML注入的可能性,从而导致XSS(跨站点脚本)攻击。


这是我在Django项目中遇到的最常见的安全问题之一。事实上,我几乎在每一个大型Django项目上以这样或那样的形式见到过它。


此外,我这里不点名指责,但我也在很多社区资源中看到过它。这包括会议演讲、博客文章和Stack Overflow的回答。


这很难做好!由于只有Django 2.1添加了json_script模板标记来安全地实现这一点,所以这也是一个历史性难题。(有关这个问题的ticket已经被开启六年了!)


让我们看看这个问题,以及我们如何使用json_script修复它。


易受攻击的方式


让我们来看看这个视图:


在Django模板中安全地包含JavaScript数据


…以及这个模板:


在Django模板中安全地包含JavaScript数据


不幸的是,正如我们所写的,该模板对HTML注入是开放的。这是因为如果数据中的任何地方包含</script>,那么结果的其余部分都会被解析为额外的HTML。我们将此称为HTML注入,攻击者可以使用它向你的站点添加任意(恶意)内容。


如果mydata可以被第三方以任何方式控制,那么,比如用户的评论或API的返回数据都可能被攻击者尝试用来进行HTML注入。


假设get_mydata()返回了这个巧妙的字符串:


在Django模板中安全地包含JavaScript数据


(我使用的是一个字符串,但这也适用于字典和列表,因为在JavaScript中它们也可以包含字符串。)


然后该模板将渲染为:


在Django模板中安全地包含JavaScript数据


浏览器首先仅通过HTML标记解析页面,而不检查其中的JavaScript。


所以它会认为第一个<script>在mydata = "之后关闭。它会尝试运行这个JavaScript,这将产生崩溃并返回一个关于不完整的字符串的错误。


然后,浏览器将解析第二个作为页面的合法部分注入的<script>标记。这意味着它会加载evil.js。


最后,它会将尾部的";作为文本进行渲染,并忽略最后的</script>,因为它没有与一个开始的<script>相匹配。


evil.js可能会做一些坏事,比如窃取你的用户的会话cookie并将其发送给攻击者。


这就惨了。


注意“safe”


如果我们不使用 |safe 的话,我们的模板将会是安全的。每当我们使用safe模板过滤器时,我们实际上是在说“我保证这些数据是安全的,可以直接包含在HTML中”。但这里不是这样。


如果我们从模板中删除它:


在Django模板中安全地包含JavaScript数据


那我们就不容易受到上面的攻击了。但数据将不会按预期的进行渲染:


在Django模板中安全地包含JavaScript数据


因为所有的HTML实体都已被转义,所以这个字符串在JavaScript中不能按预期使用。或者你需要编写额外的JavaScript来解码它们的转义,这又一次为攻击提供了机会。


另一种易受攻击的方式


另一种常见的易受攻击的模式是在视图中使用json.dumps(),并在模板中称该值为“safe”。例如,采用这个视图:


在Django模板中安全地包含JavaScript数据


…以及这个模板:


在Django模板中安全地包含JavaScript数据


这看起来更安全一些,因为我们将数据序列化为JSON,并使用了“safe”模板过滤器。不幸的是,它同样容易受到攻击,因为它也不是HTML安全的。


再想象一下,如果mydata还是和上面一样的字符串。这将使mydata_json等于:


在Django模板中安全地包含JavaScript数据


(来自json.dumps的额外双引号会将mydata转换为一个存储在Python字符串中的JSON字符串。


然后这个模板将渲染到:


在Django模板中安全地包含JavaScript数据


我们再一次遇到了同样的问题。浏览器将把HTML作为一个不完整的<script>进行解析,然后再解析另一个包含evil.js的<script>,然后是文本";,最后是一个被忽略的</script>。


安全的方式


使用Django避免这个漏洞的最佳方法是使用json_script模板标记。它会使用JSON脚本标记以防HTML注入的方式输出数据。

在我们的模板中,我们会像这样使用它:


在Django模板中安全地包含JavaScript数据


这会被渲染成这样:


在Django模板中安全地包含JavaScript数据


这是一个<script>,但由于它的类型是“application/json”而不是JavaScript类型,所以浏览器不会执行它。Django已经用JSON字符串unicode转义形式替换了所有的HTML敏感字符,比如\u003C。因此,浏览器将永远不会看到任何结束的</script>标记或类似的东西。


我们还需要更改我们的JavaScript以从该元素获取数据。改编自Django文档,最终结果将如下所示:


在Django模板中安全地包含JavaScript数据


欢呼!


CSP进阶


更新(2020-02-19): 增加了这一部分,感谢James Bligh和Tom Grainger对我的帮助。


如果你想更安全,你可以更进一步,避免在你的模板中总是使用内联<script>标签。也就是说,将你的JavaScript移到它自己的静态文件mypage.js中:


在Django模板中安全地包含JavaScript数据


然后在模板中引用它:


在Django模板中安全地包含JavaScript数据


诚然,这只是一个小小的努力。但是它可以阻止问题在你的代码中出现,因为你的JavaScript从不会经过模板。


你也可以通过在你的站点上禁止使用内联脚本来进一步降低XSS风险。你可以通过内容安全策略(CSP)使用script-src命令来实现这一点。有关更多信息,请查看我的文章《如何为你的Django网站的安全头部获得A+评分》。


“escapejs”怎么样?


更新(2020-02-19): 增加这个部分,感谢Ed Rivas在推特上提出的问题。


Django还提供了escapejs模板标记,它看起来似乎是可行的。你可能会尝试像这样来使用它:



不幸的是它并不安全,正如其文档所说:


转义字符用于JavaScript字符串中。这并不能保证在HTML或JavaScript字面量模板中使用字符串也是安全的,但是可以在你使用模板生成JavaScript/JSON时防止语法错误。


它通常也不是很有用,因为它只对字符串有效,而不是字典或列表。


结语


我希望本文有助于你编写更安全的Django应用程序。


感谢所有那些将json_script引入Django 2.1的人:


  • 感谢Gavin Wahl在django-argonauts中的初始实现

  • 感谢Matthew Schinckel 和Raphaël Hertzo在其他包中的可替代实现

  • 感谢Jonas Haag创建了针对Django的PR

  • 感谢Aymeric Augustin、Claude Paroz、Florian Apolloner、Markus Holtermann、Nick Pope、Tim Graham和Tom Forbes对这个PR的审查


—Adam


英文原文:https://adamj.eu/tech/2020/02/18/safely-including-data-for-javascript-in-a-django-template/ 
译者:浣熊君( ・᷄৺・᷅ )