当通过Javascript输出JSON内容时,我应该在服务器端还是客户端进行HTML转义

When outputting JSON content via Javascript, should I HTML escape on the server or client side?

本文关键字:服务器端 客户端 转义 HTML 我应该 Javascript 输出 JSON      更新时间:2023-09-26

我有一个应用程序,它由用PHP编写的服务器端REST API和一些使用该API并使用其生成的JSON来呈现页面的客户端Javascript组成。这是一个非常典型的设置。

REST API提供的数据是"不可信的",因为它是从数据库中获取用户提供的内容。例如,它可能会获取如下内容:

{
    "message": "<script>alert("Gotcha!")</script>"
}

显然,如果我的客户端代码要将其直接呈现到页面的DOM中,我就创建了一个XSS漏洞。因此,该内容首先需要进行html转义。

问题是,当输出不受信任的内容时,我应该在服务器端还是客户端转义该内容?也就是说,我的API应该返回原始内容,然后让客户端Javascript代码负责转义特殊字符,或者我的API应该返回"安全"内容:
{
    "message": "&lt;script&gt;alert(&#039;Gotcha!&#039;);&lt;'/script&gt;"
}

已经转义了吗?

一方面,客户端似乎不必担心来自服务器的不安全数据。另一方面,有人可能会争辩说,输出应该总是在最后一刻进行转义,当我们确切地知道如何使用数据时。

哪种方法是正确的?

注意:有很多关于处理输入的问题,是的,我知道客户端代码总是可以被操纵的。这个问题是关于服务器输出数据,这可能是不可信的。

Update:我调查了其他人在做什么,似乎有些REST api倾向于发送"不安全"的JSON。Gitter的API实际上会发送这两个,这是一个有趣的想法:

[
    {
        "id":"560ab5d0081f3a9c044d709e",
        "text":"testing the API: <script>alert('hey')</script>",
        "html":"testing the API: &lt;script&gt;alert(&#39;hey&#39;)&lt;/script&gt;",
        "sent":"2015-09-29T16:01:19.999Z",
        "fromUser":{
            ...
        },"unread":false,
        "readBy":0,
        "urls":[],
        "mentions":[],
        "issues":[],
        "meta":[],
        "v":1
    }
]

注意,它们在text键中发送原始内容,然后在html键中发送html转义版本。在我看来,这主意不错。

我已经接受了一个答案,但我不认为这是一个简单的问题。我想鼓励大家进一步讨论这个问题。

逃脱在客户端只

在客户端进行转义的原因是出于安全考虑:服务器的输出是客户端的输入,因此客户端不应该信任它。如果您假设输入已经转义,那么您可能会通过恶意反向代理等方式使自己受到客户机攻击。这与为什么您应该始终在服务器端验证输入并没有太大的不同,即使您还包括客户端验证。

在服务器端转义的原因是关注点分离:服务器不应该假设客户端打算将数据呈现为HTML。服务器的输出应该尽可能与媒体无关(当然,考虑到JSON和数据结构的约束),以便客户端可以最容易地将其转换为所需的任何格式。

对于输出进行转义:

我建议阅读这个XSS过滤器逃避小抄。

为了防止用户正确使用,您不仅要转义,还要在转义之前使用适当的反XSS库对其进行过滤。如htmllawwed,或HTML净化器,或任何来自这个线程。

我的意思是,无论何时你要在web项目中显示用户输入的数据,都应该对其进行消毒。

应该转义服务器端还是客户端上的内容?也就是说,我的API应该返回原始内容,然后让客户端Javascript代码负责转义特殊字符,或者我的API应该返回"安全"内容:

最好返回已经转义的、xss纯化的内容,因此:

  1. 获取原始数据并将其从服务器上的xss中净化
  2. 逃避
  3. 返回JavaScript

而且,你应该注意到一个重要的事情,像你的网站和读/写平衡的负载:例如,如果你的客户端输入数据一次,你要显示这个数据给100万用户,你更喜欢什么:在写之前运行保护逻辑一次(保护输入)在一百万次读(保护输出)?

如果你打算在一个页面上显示1K个帖子,并在客户端上逃避每个帖子,它在客户端的手机上工作得怎么样?最后一个选项将帮助您选择在客户端或服务器上保护数据的位置。

这个答案更侧重于争论是否进行客户端转义还是服务器端转义,因为OP似乎意识到反对在输入和输出上转义的争论。

为什么不逃避客户端?

我认为在javascript级别转义不是一个好主意。我想到的一个问题是,如果在消毒脚本中有一个错误,它将不会运行,然后允许危险的脚本运行。因此,您已经引入了一个向量,攻击者可以尝试制作输入来破坏JS消毒程序,以便允许他们的普通脚本运行。我也不知道有任何内置的反xss库在JS中运行。我相信有人已经做了一个,或者可以做一个,但是有一些已经建立的服务器端例子更值得信赖。值得一提的是,在JS中编写一个适用于所有浏览器的杀毒程序并不是一项简单的任务。

如果两个都逃逸了呢?

转义服务器端和客户端对我来说只是有点混乱,不应该提供任何额外的安全性。你提到了双重逃避的困难,我以前也经历过这种痛苦。

为什么服务器端足够好?

转义服务器端应该就足够了。你关于尽可能晚做的观点是有道理的,但我认为逃避客户端的缺点被你可能从中获得的任何微小好处所抵消。威胁在哪里?如果攻击者存在于您的站点和客户端之间,那么客户端已经受到了威胁,因为如果他们想要的话,他们可以发送一个带有脚本的空白html文件。您需要尽最大努力发送安全的东西,而不仅仅是发送处理危险数据的工具。

TLDR;如果您的API要传递格式化信息,它应该输出HTML编码字符串。警告:任何消费者都需要相信你的API不会输出恶意代码。内容安全策略也可以帮助解决这个问题。

如果您的API只输出纯文本,则在客户端进行HTML编码(因为纯文本中的<在任何输出中也意味着<)。

Not too long, Not done reading:

如果你同时拥有API和web应用程序,任何一种方式都是可以接受的。只要你不输出JSON到HTML页面没有十六进制实体编码像这样:
<%
payload = "[{ foo: '" + foo + "'}]"
%>
    <script><%= payload %></script>

那么无论服务器上的代码是将&更改为&amp;还是浏览器中的代码将&更改为&amp;都无关紧要。

让我们以你的问题为例:

[
    {
        "id":"560ab5d0081f3a9c044d709e",
        "text":"testing the API: <script>alert('hey')</script>",
        "html":"testing the API: &lt;script&gt;alert(&#39;hey&#39;)&lt;/script&gt;",
        "sent":"2015-09-29T16:01:19.999Z",

如果上面是从api.example.com返回的,并且您从www.example.com调用它,因为您控制了两边,您可以决定是否要采用纯文本"text"或格式化文本"html"。

重要的是要记住,插入到html中的任何变量都是在服务器端进行HTML编码的。并且还假设已经执行了正确的JSON编码,可以阻止任何引号字符中断或更改JSON的上下文(为了简单起见,上面没有显示)。

text将被插入到使用Node.textContenthtml作为Element.innerHTML的文档中。使用Node.textContent将导致浏览器忽略可能存在的任何HTML格式和脚本,因为像<这样的字符在页面上被当作<输出。

注意,您的示例显示了作为脚本输入的用户内容。例如,用户在应用程序中输入了<script>alert('hey')</script>,它的不会生成 API。如果您的API实际上想要输出标记作为其功能的一部分,那么它必须将它们放在JSON中:

"html":"<u>Underlined</u>"

然后你的text 必须只输出文本而不格式化:

"text":"Underlined"

因此,你的API在向你的web应用程序用户发送信息时不再传输富文本,只传输纯文本。

但是,如果第三方正在使用您的API,那么他们可能希望以纯文本形式从您的API获取数据,因为这样他们就可以在客户端自己设置Node.textContent(或HTML编码),知道它是安全的。如果你返回HTML,那么你的消费者需要相信你的HTML不包含任何恶意脚本。

因此,如果上述内容来自api.example.com,但您的消费者是第三方站点,例如www.example.edu,那么他们可能更愿意接受text而不是HTML。在这种情况下,您的输出可能需要更细粒度地定义,因此与其输出

"text":"Thank you Alice for signing up."

输出

[{ "name", "alice",
"messageType": "thank_you" }]

或类似的,所以你不再在JSON中定义布局,你只是向客户端传达信息,让他们使用自己的风格来解释和格式化。为了进一步澄清我的意思,如果你的消费者得到的是

"text":"Thank you Alice for signing up."

并且他们想以粗体显示名称,如果没有复杂的解析,对他们来说完成这一点将是非常棘手的。然而,通过在粒度级别上定义API输出,消费者可以像变量一样获取输出的相关部分,然后应用他们自己的HTML格式,而不必信任您的API只输出粗体标记(<b>)而不输出恶意JavaScript(无论是来自用户还是来自您,如果您确实是恶意的,或者如果您的API已被泄露)。