在DOM中嵌入任意JSON的最佳实践是什么?

135

我正在考虑像这样在DOM中嵌入任意的JSON:

<script type="application/json" id="stuff">
    {
        "unicorns": "awesome",
        "abc": [1, 2, 3]
    }
</script>

这类似于将任意HTML模板存储在DOM中,以备稍后使用JavaScript模板引擎。在这种情况下,我们稍后可以检索JSON并使用以下方式解析:

var stuff = JSON.parse(document.getElementById('stuff').innerHTML);

这样做是可行的,但它是否是最佳方式?这是否违反了任何最佳实践或标准?

注意:我不是在寻找将JSON存储在DOM中的替代方案,我已经决定这是我遇到的特定问题的最佳解决方案。我只是想找到最佳方法来实现它。


2
为什么你不把它声明为JavaScript中的“var”? - Krizz
@Krizz,它需要成为静态文档的一部分,然后由封装的复杂JavaScript链进行处理。将其存储在DOM中是我想要做的。 - Ben Lee
@Krizz 我曾经遇到过类似的问题。我想为每个用户将数据放在不同的站点上,而不必进行 AJAX 请求。因此,我在一个容器中嵌入了一些 PHP,并执行了与您上面所做的类似的操作,以便在 JavaScript 中获取数据。 - Patrick Lorio
2
我认为你原来的方法其实是最好的。它在HTML5中是100%有效的,它具有表现力,不会创建您只需使用CSS删除或隐藏的"虚假"元素;而且它不需要任何字符编码。有什么缺点吗? - Jamie Treworgy
26
如果你的JSON对象中包含值为</script><script>alert()</script><script>的字符串,那么你将会得到意外的结果。除非你先对数据进行清洁处理,否则这是不安全的。 - silviot
@silviot 如果我盲目地将JSON字符串中的任何<替换为\u003C,这样就足够了吗?这似乎太简单了,真的有效吗... - badp
8个回答

90

我认为你原本的方法是最好的。HTML5规范甚至提到了这种用法:

“当用于包含数据块(而不是脚本)时,数据必须内联嵌入,必须使用type属性给出数据格式,不能指定src属性,并且脚本元素的内容必须符合所用格式的要求。”

请点击这里查看:http://dev.w3.org/html5/spec/Overview.html#the-script-element

你已经完全做到了。有什么不喜欢的呢?不需要像属性数据一样进行字符编码。如果你想要,可以进行格式化。它很具表现力,预期用法也很清晰。它不像一个hack(例如使用CSS来隐藏你的“载体”元素)。它是完全有效的。


3
谢谢。规范中的引语已经说服了我。 - Ben Lee
18
只有在您先检查并清洗JSON对象后,才是完全有效的:您不能只是嵌入用户生成的数据。请参见我在问题上的评论。 - silviot
1
额外的疑惑:应该把它放在什么地方?头部还是正文部分,顶部还是底部? - challet
2
不幸的是,CSP策略可能会阻止所有script标签。参考链接 - Larry K
3
如何有效地防止嵌入含有 "</script>" 的 JSON 数据,从而避免 HTML 注入?是否有坚实/简单的方案,还是最好使用数据属性? - jonasfj

21

作为一般方向,我会尝试使用 HTML5数据属性。这里没有阻止您放入有效的JSON。例如:

<div id="mydiv" data-unicorns='{"unicorns":"awesome", "abc":[1,2,3]}' class="hidden"></div>
如果你正在使用jQuery,那么获取它就像这样简单:
var stuff = JSON.parse($('#mydiv').attr('data-unicorns'));

1
有意义。但请注意,对于键名使用单引号时,JSON.parse 无法工作(至少原生的 Google Chrome JSON.parse 无法)。JSON 规范要求使用双引号。但是,使用实体(如 ...&lt;unicorns&gt;:...)很容易解决这个问题。 - Ben Lee
5
有一个问题:HTML 5 中属性的长度是否有限制? - Ben Lee
是的,那样做可以。您还可以将其反过来,使您的HTML使用单引号,而JSON数据使用双引号。 - Horatio Alderaan
1
好的,我找到了问题的答案:https://dev59.com/bnI_5IYBdhLWcg3wK_zE -- 这对我的目的已经足够了。 - Ben Lee
5
这对于单个字符串是行不通的,例如 "I am valid JSON" 并且使用双引号作为标签,或者在字符串中使用单引号,例如 data-unicorns='"My JSON's string"' 因为单引号在编码为 JSON 时没有被转义。 - scrowler
你可以始终使用 escape(JSON.stringify({str: "'"})) - andrsnn

16

在脚本标签中嵌入json的方法存在潜在的安全问题。假设json数据来自用户输入,可能会创建一个数据成员,实际上会跳出脚本标签,并允许直接注入到dom中。参见此处:

http://jsfiddle.net/YmhZv/1/

这里是注入:

<script type="application/json" id="stuff">
{
    "unicorns": "awesome",
    "abc": [1, 2, 3],
    "badentry": "blah </script><div id='baddiv'>I should not exist.</div><script type="application/json" id='stuff'> ",
}
</script>

没有绕过转义/编码的方法。


9
这是正确的,但它不是该方法真正存在安全漏洞。如果你在页面上使用来自用户输入的信息,你必须注意对其进行转义处理。只要你采取了关于用户输入的常规预防措施,这种方法仍然是可靠的。 - Ben Lee
JSON不是HTML的一部分,HTML解析器只会继续执行。这与当JSON是文本段落或div元素的一部分时相同。在程序中对内容进行HTML转义。此外,您还可以转义斜杠。虽然JSON不需要这样做,但它确实容忍不必要的斜杠。这可用于使其安全嵌入。PHP的json_encode默认情况下会执行此操作。 - Timo Tijhof

10

请参见OWASP的XSS预防技巧清单中的第3.1条规则

假设您想在HTML中包含此JSON:

{
    "html": "<script>alert(\"XSS!\");</script>"
}

在HTML中创建一个隐藏的<div>。接下来,通过编码不安全字符实体(例如&、<、>、"、'和/)对JSON进行转义,并将其放置在该元素中。

<div id="init_data" style="display:none">
        {&#34;html&#34;:&#34;&lt;script&gt;alert(\&#34;XSS!\&#34;);&lt;/script&gt;&#34;}
</div>

现在你可以使用JavaScript读取元素的textContent并解析它来访问它:

var text = document.querySelector('#init_data').textContent;
var json = JSON.parse(text);
console.log(json); // {html: "<script>alert("XSS!");</script>"}

1
我相信这是最好且最安全的答案。请注意,许多常见的JSON字符被转义,并且某些字符被双重转义,例如对象{name: 'Dwayne "The Rock" Johnson'}中的内部引号。但是,最好使用此方法,因为您的框架/模板库可能已经包含了一种安全的方式来进行HTML编码。另一种选择是使用base64,它既可以在HTML中使用,也可以安全地放置在JS字符串中。在JS中使用btoa()/atob()很容易进行编码/解码,而且对于您来说,在服务器端也很容易实现。 - sstur
一种更安全的方法是使用语义正确的<data>元素,并将JSON数据包含在value属性中。然后,您只需要使用&quot转义引号,如果您使用双引号括起数据,或者使用&#39;如果您使用单引号(这可能更好)。 - Rúnar Berg

9

HTML5包括一个<data>元素,用于保存机器可读的数据。可以将JSON数据放在该元素的value属性中,这是一种也许更安全的替代方法,比使用<script type="application/json">标签更为可靠。

const jsonData = document.querySelector('.json-data');
const data = JSON.parse(jsonData.value);

console.log(data)
<data class="json-data" value='
  {
    "unicorns": "awesome",
    "abc": [1, 2, 3],
    "careful": "to escape &#39; quotes"
  }
'></data>

在这种情况下,您需要用&#39;替换所有单引号,如果您选择用双引号包含该值,则需要用&quot;替换。否则,您将冒着XSS攻击的风险,就像其他答案所建议的那样。


6

我建议将JSON放入带有函数回调的内联脚本中(类似于JSONP):

<script>
someCallback({
    "unicorns": "awesome",
    "abc": [1, 2, 3]
});
</script>

如果执行脚本在文档加载后加载,您可以将其存储在某个地方,可能带有附加的标识符参数:someCallback("stuff", { ... });


@BenLee 它应该能够很好地工作,唯一的缺点是必须定义回调函数。另一个建议的解决方案在特殊的HTML字符(例如&)和引号中断,如果您在JSON中有这些字符。 - copy
这样做感觉更好,因为您不需要进行DOM查询来查找数据。 - Jaseem
@copy 这个解决方案仍然需要转义(只是不同的方式),请参考MadCoder的答案。我在这里只是为了完整性而留下它。 - pvgoran
这似乎会更快,因为对象直接被解析为JavaScript,而不是从DOM获取文本并通过JSON.parse进行处理。在我的网站上,我使用const MY_DATA = ... - Indiana Kernick

1

我的建议是将JSON数据保存在外部的.json文件中,然后通过Ajax检索这些文件。你不会将CSS和JavaScript代码放到网页上(内联),那么为什么要这样做呢?


16
在网页中不要将CSS和Javascript放入内联,因为这些通常在其他页面之间共享。如果所讨论的数据是由服务器显式地生成用于此上下文环境,嵌入它比发起另一个无法被缓存的请求更有效率。 - Jamie Treworgy
这是因为我正在更新一个设计不良的遗留系统,而不是重新设计整个系统,我只需要修复其中的一部分。在DOM中存储JSON是修复这一部分的最佳方法。此外,我同意@jamietre所说的话。 - Ben Lee
请注意,OP指出这个JSON字符串只在后面需要。问题是它是否总是需要,还是只在某些情况下需要。如果它只在某些情况下需要,那么将其放在外部文件中并仅在条件加载时加载是有意义的。 - Šime Vidas
3
我同意,有许多“如果”的情况可能会倾向于某一方。但一般来说,如果你知道在页面渲染时你将需要什么——即使只是可能需要——那么最好立即发送它上去。比如,如果我有一些信息框开始是折叠的,我通常会喜欢将它们的内容内联包含以便它们可以立即展开。与新请求相比,额外数据对现有请求的开销要小得多,这创造了更具响应性的用户体验。我确定存在一个临界点。 - Jamie Treworgy
也许应该将其删除?它没有回答问题。OP并没有询问“如何从服务器获取数据到客户端”的一般性问题,如果他们这样做了,那么这个问题很可能会因为过于宽泛而被删除。 - Abhi Beckert

0

我正在查看一些网站的代码,发现他们在JavaScript中使用了JSON,这就是魔法词。如果你想在HTML中使用JSON,只需做两件事:html>JavaScript>JSON。

<script>
//whatever code to put json in JavaScript(idk)
</script>

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