如何在HTML的script标签中插入任意JSON

18

我想要将JSON的内容存储在HTML文档的源码中,放在一个脚本标签内。

该JSON的内容取决于用户提交的输入,因此需要非常小心地对该字符串进行XSS防护。

我在这里读到了两个概念。

1. 将所有出现的</script标签替换为<\/script,或者在服务器端将所有</替换为<\/

代码示例如下(使用Python和jinja2作为示例):

// view
data = {
    'test': 'asdas</script><b>as\'da</b><b>as"da</b>',
}

context_dict = {
    'data_json': json.dumps(data, ensure_ascii=False).replace('</script', r'<\/script'),
}

// template
<script>
    var data_json = {{ data_json | safe }};
</script>

// js
access it simply as window.data_json object

2. 将数据编码为HTML实体编码的JSON字符串,并在客户端解码+解析。 解码参考这个回答:https://dev59.com/o3I-5IYBdhLWcg3wVWpi#34064434

// view
context_dict = {
    'data_json': json.dumps(data, ensure_ascii=False),
}

// template
<script>
    var data_json = '{{ data_json }}'; // encoded into HTML entities, like &lt; &gt; &amp;
</script>

// js
function htmlDecode(input) {
  var doc = new DOMParser().parseFromString(input, "text/html");
  return doc.documentElement.textContent;
}

var decoded = htmlDecode(window.data_json);
var data_json = JSON.parse(decoded);
该方法无法使用,因为在脚本源代码中的 \" 在 JS 变量中变成了 "。此外,它创建了一个更大的 HTML 文档,也不是真正可读的,所以如果它不意味着巨大的安全风险,我会选择第一个版本。
在使用第一个版本时是否存在任何安全风险?使用 .replace('</script', r'<\/script') 来净化 JSON 编码字符串是否足够?
关于此问题的参考资料:
在 HTML 属性中存储 JSON 的最佳方法?
在使用 document.write() 写入时为什么要拆分 <script> 标签?
JavaScript 字符串中的脚本标记
净化 <script> 元素内容
转义脚本标记内容中的 </ 一些关于此问题的优秀外部资源:
Flask 的 tojson 过滤器实现源代码
Rail 的 json_escape 方法的帮助文档源代码
Django 中长达 5 年的讨论票务系统建议的代码

4
你应该将 <, >, 和 & 进行 HTML 实体编码。 - Pointy
6
我花了一个小时写这个问题,包括引用我找到的所有之前的SO答案。收到一句话和关闭/-1的回答感觉一点也不有帮助。 - hyperknot
2
至少 JSON.stringify() 和 Python 的 json.dumps() 不会将 / 转义为 \/。我正在寻找一种自动化的方式,可以使用脚本标签解析器来解码 JSON 或对字符串使用 JSON.parse()。在服务器端手动转义需要在客户端也进行手动操作。 - hyperknot
3
自上次评论以来,我发现Flask中的“|tojson”过滤器实现是最好的资源。那里有源代码以及一些非常重要的注释。https://github.com/pallets/flask/blob/78a71a48dcb71cb930d747d9facef0dfa5a8f022/flask/json.py#L158我对正确方法的理解如下:
  1. 使用我问题中的方法1。
  2. 将<, >, &和'编码为u00格式(不是HTML实体!)。
  3. 仔细检查JSON编码器是否转义\\,因为它取决于实现方式(有时甚至会在版本中间更改)。
- hyperknot
2
请查看http://archive.oreilly.com/pub/a/actionscript/excerpts/as3-cookbook/appendix.html,获取“u00”替代符号列表,用于替换<、>、&、引号和斜杠。 - catamphetamine
显示剩余6条评论
2个回答

6
这是我处理此问题相对较小的部分——在脚本元素中存储JSON时的编码问题。简单地说,您必须转义</,因为它们一起终止脚本元素——即使在JSON字符串文字中也是如此。您不能为脚本元素HTML编码实体。您可以JavaScript反斜杠转义斜杠。我更喜欢JavaScript十六进制转义小于号角括号,如\u003C

.replace('<', r'\u003C')

我在尝试传递oembed结果的json时遇到了这个问题。其中一些包含脚本关闭标记(没有提到Twitter的名称)。

json_for_script = json.dumps(data).replace('<', r'\u003C');

这将 data = {'test': 'foo </script> bar'}; 转化为
'{"test": "foo \\u003C/script> bar"}'

这是一个有效的JSON,不会中断脚本元素。

我从Jinja模板引擎内部的小宝石得到了这个想法。当您使用{{data|tojson}} 过滤器时,它会运行。

def htmlsafe_json_dumps(obj, dumper=None, **kwargs):
    """Works exactly like :func:`dumps` but is safe for use in ``<script>``
    tags.  It accepts the same arguments and returns a JSON string.  Note that
    this is available in templates through the ``|tojson`` filter which will
    also mark the result as safe.  Due to how this function escapes certain
    characters this is safe even if used outside of ``<script>`` tags.
    The following characters are escaped in strings:
    -   ``<``
    -   ``>``
    -   ``&``
    -   ``'``
    This makes it safe to embed such strings in any place in HTML with the
    notable exception of double quoted attributes.  In that case single
    quote your attributes or HTML escape it in addition.
    """
    if dumper is None:
        dumper = json.dumps
    rv = dumper(obj, **kwargs) \
        .replace(u'<', u'\\u003c') \
        .replace(u'>', u'\\u003e') \
        .replace(u'&', u'\\u0026') \
        .replace(u"'", u'\\u0027')
    return Markup(rv)

您可以在脚本元素中使用\x3C代替\u003C,因为它是有效的JavaScript。但最好坚持使用有效的 JSON。


@hyperknot,现在我看到了你的评论,你在几年前链接到了这个相同的例程。天啊,我希望我早点看到它。我经历了一段寻找它的奥德赛。不过没关系,这实际上是令人放心的。我会让这个答案保持原样,因为.replace('<', r'\x3C')是我认为对你的问题有用的答案。 - Bob Stein

1

首先,你的偏执是有根据的。

  • HTML解析器可能会被关闭脚本标签欺骗(最好假设任何关闭标签都可能)
  • JS解析器可能会被反斜杠和引号欺骗(使用非常糟糕的编码器)

是的,将所有可能混淆不同解析器的字符进行编码会更加“安全”。保持可读性可能与您的安全范例相矛盾。

注意:JSON字符串编码的结果应该是规范的,并且当然不能出现错误,可以被解析。JSON是JS的子集,因此可以在没有任何风险的情况下进行JS解析。因此,您需要确保提取JS代码的HTML解析器实例不会被用户数据欺骗。

因此,真正的陷阱是两个解析器的嵌套。实际上,我建议您将这样的内容放入单独的请求中。这样,您就可以完全避免这种情况。

假设在这样的解析器中可能发生的所有可能的样式和错误修正中,其他标记(开放或关闭)可能会实现类似的功能。

例如:向解析器建议脚本标签已经隐式结束。

因此,建议对斜杠和所有标签括号(/,<,>)进行编码,而不仅仅是脚本标记的关闭,使用任何可逆转的方法,只要不会混淆HTML解析器:

  • 最好的选择是base64(但您希望更易读)
  • HTML实体也可以,尽管会让人困惑 :)
  • 自己进行转义也可以,只需转义单个字符而不是</script片段

总之,是的,最好进行一些更改,但请注意,与其通过XHR加载JSON或至少使用严格的字符串编码(如base64),不如首先尝试这样做,这样您就已经离“安全”只有一步之遥了。

P.S.:如果您可以从其他人的代码中学习字符串编码,那很好,但如果它们不能完全满足您的需求,就不应该使用“库”或其他人的函数。因此,最好编写并彻底测试自己的(de/en)coder,并知道这个陷阱已经被封闭。


HTML解析器并不会被闭合的script标签所“欺骗”,它是以必要且有文档规定的方式识别所谓的不可替换字符数据中的结束标签。 - Kaz

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接