Django模板通常用于将数据传递到JavaScript代码。 不幸的是,如果实施不正确,这将打开HTML注入的可能性,从而引发XSS(跨站点脚本)攻击。
这是我在Django项目中遇到的最常见的安全问题之一。 实际上,我可能已经在每个相当大的Django项目中以某种形式看到了它。
很难正确! 从历史版本上看也很困难,因为只有Django 2.1才添加了json_script模板标签来安全地执行此操作。
让我们看一下问题以及如何使用json_script修复它。
危险的方法:
让我们看一下这个view:
1 2 3 4 5 6 |
from django.shortcuts import render def index(request): mydata = get_mydata() return render(request, 'index.html', context={"mydata": mydata}) |
模板:
1 2 3 |
<script> const mydata = "{{ mydata|safe }}"; </script> |
不幸的是,该模板可以进行HTML注入。 这是因为如果数据在任何地方都包含</ script>,则结果的其余部分将被解析为额外的HTML。 我们称这种HTML注入,攻击者可以使用它向您的网站添加任意(邪恶的)内容。
如果mydata可由第三方以任何方式控制,例如用户的注释或API的返回数据,则攻击者可能会尝试将其用于HTML注入。
想象一下get_mydata()返回了这个狡猾的字符串:
1 |
'</script><script src="https://example.com/evil.js"></script>' |
(我使用的是字符串,但这也适用于字典和列表,因为在JavaScript中它们也可以包含字符串。)
然后,该模板将呈现为:
1 2 3 |
<script> const mydata = "</script><script src="https://example.com/evil.js"></script>"; </script> |
浏览器首先仅通过HTML标签解析页面-无需检查其中的JavaScript。
因此,它会看到第一个<script>在mydata =“之后关闭。它将尝试运行该JavaScript,它将因不完整的字符串错误而崩溃。
然后它将解析第二个注入的<script>标记作为页面的合法部分。 这意味着它将加载evil.js。
最后,它将尾随的“;”呈现为文本,并忽略最后一个</ script>,因为它与开头的<script>不匹配。
evil.js可能会做一些恶作剧,例如窃取用户的会话Cookie并将其发送给攻击者。
当心“safe”
如果不使用| safe,我们的模板将是安全的。 每当我们使用安全模板过滤器时,我们真正要说的是“我保证此数据可以直接包含在HTML中是安全的”。 事实并非如此。
如果我们将其从模板中删除:
1 2 3 |
<script> const mydata = "{{ mydata }}"; </script> |
那么我们就不会对上述攻击持开放态度。 但是数据不会按预期呈现:
1 2 3 |
<script> const mydata = "</script><script src="https://example.com/evil.js"></script>"; </script> |
由于所有HTML实体均已转义,因此该字符串将无法按预期在JavaScript中使用。 否则,您需要编写额外的JavaScript来使其解脱,这也为再次攻击提供了机会。
另一种脆弱的方式
另一个常见的易受攻击的模式是在视图中使用json.dumps(),然后在模板中将该值称为“安全”。 例如,采取以下view:
1 2 3 4 5 6 7 |
import json from django.shortcuts import render def index(request): mydata = get_mydata() return render(request, 'index.html', context={"mydata_json": json.dumps(mydata)}) |
模板:
1 2 3 |
<script> const mydata = {{ mydata_json|safe }}; </script> |
这看起来更安全,因为我们将数据序列化为JSON,并使用“安全”模板过滤器。 不幸的是,它同样脆弱,因为它也不安全。
再次想象一下,mydata还是与上面相同的字符串。 那将使mydata_json等于:
1 |
'"</script><script src=\"https://example.com/evil.js\"></script>"' |
(在json.dumps中加双引号,将其转换为存储在Python字符串中的JSON字符串。)
然后,该模板将呈现为:
1 2 3 |
<script> const mydata = "</script><script src="https://example.com/evil.js"></script>"; </script> |
同样,我们有同样的问题。 浏览器会将HTML解析为不完整的<script>,然后解析为另一个<script>以包含evil.js,然后包含文本“;”,最后是被忽略的</ script>
安全方式
使用Django避免此漏洞的最佳方法是使用json_script模板标签。 这通过使用JSON脚本标签以HTML注入证明的方式输出数据。
在我们的模板中,我们将像这样使用它:
1 |
{{ mydata|json_script:"mydata" }} |
将被渲染为:
1 |
<script id="mydata" type="application/json">"\u003C/script\u003E\u003Cscript src=\"https://example.com/evil.js\"\u003E\u003C/script\u003E"</script> |
这是一个<script>,但是由于其类型是“ application / json”而不是JavaScript类型,因此浏览器不会执行它。 Django已将所有HTML敏感字符替换为其JSON字符串Unicode转义形式,例如\ u003C。 因此,浏览器将永远不会看到任何结束</ script>标记或类似标记。
我们还需要更改JavaScript以从该元素中获取数据。 改编自Django文档,最终结果如下所示:
1 2 3 4 |
{{ mydata|json_script:"mydata" }} <script> const mydata = JSON.parse(document.getElementById('mydata').textContent); </script> |
与CSP同行
如果您想更加安全,可以再走一步,避免在模板中完全使用内联<script>标记。 也就是说,将您的JavaScript移到其自己的静态文件mypage.js中:
1 |
const mydata = JSON.parse(document.getElementById('mydata').textContent); |
模板:
1 2 |
{{ mydata|json_script:"mydata" }} <script src="{% static 'mypage.js' %}"></script> |
您可以通过禁止站点上的内联脚本来进一步降低XSS风险。 您可以使用script-src指令通过内容安全策略(CSP)来实现。 有关更多信息,请参阅我的文章“如何在Django网站上为安全标头评分A +”。
那关于“ escapejs”呢?
Django还提供了escapejs模板标记,看起来它可以工作。 您可能会想这样使用它:
1 2 3 |
<script> const mydata = "{{ mydata|escapejs }}"; </script> |
如文档所述,不幸的是并不安全:
转义用于JavaScript字符串的字符。 这不能确保在HTML或JavaScript模板文字中使用该字符串的安全性,但是可以在使用模板生成JavaScript / JSON时保护您免受语法错误的影响。
通常它也没有用,因为它仅适用于字符串,不适用于字典或列表。
最后
我希望这可以帮助您编写更安全的Django应用程序。
原文:https://adamj.eu/tech/2020/02/18/safely-including-data-for-javascript-in-a-django-template/