前后端分离的思考与实践(四)
作者: 发布于:

前后端分离模式下的安全解决方案

前言

在前后端分离的开发模式中,从开发的角色和职能上来讲,一个最明显的变化就是:以往传统中,只负责浏览器环境中开发的前端同学,需要涉猎到服务端层面,编写服务端代码。而摆在面前的一个基础性问题就是如何保障 Web 安全?

本文就在前后端分离模式的架构下,针对前端在 Web 开发中,所遇到的安全问题以及应对措施和注意事项,并提出解决方案。

跨站脚本攻击(XSS)的防御

问题及解决思路

跨站脚本攻击(XSS,Cross-site scripting)是最常见和基本的攻击 Web 网站的方法。攻击者可以在网页上发布包含攻击性代码的数据,当浏览者看到此网页时,特定的脚本就会以浏览者用户的身份和权限来执行。通过 XSS 可以比较容易地修改用户数据、窃取用户信息以及造成其它类型的攻击,例如:CSRF 攻击。

预防 XSS 攻击的基本方法是:确保任何被输出到 HTML 页面中的数据以 HTML 的方式进行转义(HTML escape)。例如下面的模板代码:

<textarea name="description">$description</textarea>

这段代码中的 $description 为模板的变量(不同模板中定义的变量语法不同,这里只是示意一下),由用户提交的数据,那么攻击者可以输入一段包含 JavaScript 的代码,使得上述模板语句的结果变成如下的结果:

<textarea name="description">
</textarea><script>alert('hello')'</script>
</textarea>

上述代码,在浏览器中渲染,将会执行 JavaScript 代码并在屏幕上 alert hello。当然这个代码是无害的,但攻击者完全可以创建一个 JavaScript 来修改用户资料或者窃取 cookie 数据。

解决方法很简单,就是将 $description 的值进行 HTML escape,转义后的输出代码如下

<textarea name="description">
&lt;/textarea&gt;&lt;script&gt;alert(&quot;hello!&quot;)&lt;/script&gt;
</textarea>

以上经过转义后的 HTML 代码是没有任何危害的。

Midway 的解决方案

转义页面中所有用户输出的数据

对数据进行转义有以下几种情况和方法:

1. 使用模板内部提供的机制进行转义

中途岛内部使用 KISSY XTemplate 作为模板语言。

在 XTemplate 实现中,语法上使用两个中括号({% raw %}{{val}}{% endraw %})解析模板数据,默认既是对数据进行 HTML 转义的,所以开发者可以这样写模板:

<textarea name="description">{{description}}</textarea>

在 XTemplate 中,如果不希望输出的数据被转义,需要使用三个中括号({% raw %}{{{val}}}{% endraw %})。

2. 在Midway中明确的调用转义函数

开发者可以在 Node.js 程序或者模板中,直接调用 Midway 提供的 HTML 转义方法,显示的对数据进行转义,如下:

方法1:在 Node.js 程序中对数据进行 HTML 转义

var Security= require('midway-security');
//data from server,eg {html:'</textarea>',other:""}
data.html =Security.escapeHtml(data.html);
xtpl = xtpl.render(data);

方法2:在模板中对 HTML 数据进行 HTML 转义

<textarea name="description">Security.escapeHtml({{{description}}})</textarea>

注意:只有当模板内部没有对数据进行转义的时候才使用 Security.escapeHtml 进行转义。 否则,模板内部和程序会两次转义叠加,导致不符合预期的输出。

推荐:如果使用 XTemplate,建议直接使用模板内置的 {% raw %}{{}}{% endraw %} 进行转义; 如果使用其他模板,建议使用 Security.escapeHtml 进行转义。

过滤页面中用户输出的富文本

你可能会想到:“其实我就是想输出富文本,比如一些留言板、论坛给用户提供一些简单的字体大小、颜色、背景等功能,那么我该如何处理这样的富文本来防止 XSS 呢?”

1. 使用 Midway 中 Security 提供的 richText 函数

Midway 中提供了 richText 方法,专门用来过滤富文本,防止 XSS、钓鱼、cookie 窃取等漏洞。

有一个留言板,模板代码可能如下:

<div class="message-board">
{{{message}}}
</div>

因为 message 是用户的输入数据,其留言板的内容,包含了富文本信息,所以这里 XTemplate 中,使用了三个大括号,默认不进行 HTML 转义;那么用户输入的数据假如如下:

<script src="http://eval.com/eval.js"></script><span style="color:red;font-size:20px;position:fixed;">我在留言中</span>

上述的富文本数据如果直接输出到页面中,必然会导致 eval.com 站点的 JS 注入到当前页面中,造成了 XSS 攻击。为了防止这个漏洞,我们只要在模板或者程序中,调用 Security.richText 方法,处理用户输入的富文本。

调用方法与 escapeHtml 类似,有如下两种方式

方法1: 直接在 Node.js 程序中调用

message = Security.richText(message);
var html = xtpl.render(message);

方法2: 在模板中调用

<div class="message-board">
Security.richText({{{message}}})
</div>

通过调用 Security 的 richText 方法后,最终的输出如下:

<div class="message-board">
<span style="color:red;font-size:20px;">我在留言中</span>
</div>

可以看出,首先:会造成 XSS 攻击的 script 标签被直接过滤掉;同时 style 标签中 CSS 属性 position:fixed; 样式也被过滤了。最终输出了无害的 HTML 富文本。

了解其他可能导致 XSS 攻击的途径

除了在页面的模板中可能存在 XSS 攻击之外,在 Web 应用中还有其他几个途径也可能会有风险。

1. 出错页面的漏洞

一个页面如果找不到,系统可能会报一个 404 Not Found 的错误,例如:http://localhost/page/not/found

404 NotFound
Page /page/not/found does not exsit

很显然:攻击者可以利用这个页面,构造一个类似这样的连接,http://localhost/%3Cscript%3Ealert%28%27hello%27%29%3C%2Fscript%3E,并引诱受害者点击 ;假如出错页面未对输出变量进行转义的话,那么连接中隐藏的 <script>alert('hello')</script> 将会被执行。

在 Express 中,发送一个 404 页面的方法如下

res.send(404,'Sorry,we don\'t find that!')

这里就需要开发者注意错误页面(404 或者其他错误状态)的处理方式。如果错误信息的返回内容带有路径信息(其实更准确的讲,是用户输入信息),就一定要进行 escapeHtml 了。

后续,错误处理的安全机制,会在 Midway 框架层面中完成。

Midway 解决方案的补充说明

其他模板引擎

Midway 默认支持 XTemplate 模板,但将来也有可能支持其他模板:如 jade、mustache、ejs 等。目前在主流模板中,都提供了默认转义和不转义的输出变量写法,需要开发者特别留意其安全性。

关于 escape 的其他支持

除了对页面中输出的普通数据和富文本数据,一些场景中也还包含其他可能需要转义的情况,Midway 提供了如下几个常用的转义方法,供开发者使用:

  • escapeHtml 过滤指定的 HTML 中的字符,防 XSS 漏洞
  • jsEncode 对输入的 String 进行 JavaScript 转义 对中文进行 unicode 转义,单引号,双引号转义
  • escapeJson 不破坏 JSON 结构的 escape 函数,只对 json 结构中 name 和 vaule 做 escapeHtml 处理
  • escapeJsonForJsVar 可以理解就是 jsEncode + escapeJson

例子如下

var jsonText = '{"<script>":"<script>"}';

console.log(SecurityUtil.escapeJson(jsonText)); // {"&lt;script&gt;":"&lt;script&gt;"}

var jsonText ='{"你好":"<script>"}';

console.log(SecurityUtil.escapeJsonForJsVar(jsonText)); //{\"\u4f60\u597d\":\"&lt;script&gt;\"}

var str ="alert(\"你好\")";

console.log(SecurityUtil.jsEncode(str)); // alert(\"\u4f60\u597d\")

跨站请求伪造攻击(CSRF)的预防

问题及解决思路

名词解释: 表单:泛指浏览器端用于客户端提交数据的形式;包括 a 标签、AJAX 提交数据、form 表单提交数据等,而非对等于 HTML 中的 form 标签。

跨站请求伪造(CSRF,Cross-site request forgery)是另一种常见的攻击。攻击者通过各种方法伪造一个请求,模仿用户提交表单的行为,从而达到修改用户的数据或执行特定任务的目的。

为了假冒用户的身份,CSRF 攻击常常和 XSS 攻击配合起来做,但也可以通过其它手段:例如诱使用户点击一个包含攻击的链接。

解决 CSRF 攻击的思路分如下两个步骤

  • 增加攻击的难度。GET 请求是很容易创建的,用户点击一个链接就可以发起 GET 类型的请求,而 POST 请求相对比较难,攻击者往往需要借助 JavaScript 才能实现;因此,确保 form 表单或者服务端接口只接受 POST 类型的提交请求,可以增加系统的安全性。
  • 对请求进行认证,确保该请求确实是用户本人填写表单或者发起请求并提交的,而不是第三者伪造的。

一个正常用户修改网站信息的过程如下

  • 用户请求修改信息(1) -> 网站显示用户修改信息的表单(2) -> 用户修改信息并提交(3) -> 网站接受用户修改的数据并保存(4)

而一个 CSRF 攻击则不会走这条路线,而是直接伪造第 2 步用户提交信息

  • 直接跳到第 2 步(1) -> 伪造要修改的信息并提交(2) -> 网站接受攻击者修改参数数据并保存(3)

只要能够区分这两种情况,就能够预防 CSRF 攻击。那么如何区分呢? 就是对第2步所提交的信息进行验证,确保数据源自第一步的表单。具体的验证过程如下:

  • 用户请求修改信息(1) -> 网站显示用于修改信息的空白表单,表单中包含特殊的 token 同时把 token 保存在 session 中(2) -> 用户修改信息并提交,同时发回 token 信息到服务端(3) -> 网站比对用户发回的 token 和 session 中的 token,应该一致,则接受用户修改的数据,并保存

这样,如果攻击者伪造要修改的信息并提交,是没办法直接访问到 session 的,所以也没办法拿到实际的 token 值;请求发送到服务端,服务端进行 token 校验的时候,发现不一致,则直接拒绝此次请求。

Midway 解决方案

禁用 GET 提交表单

如果服务端不接受 GET 方式提交的表单数据,那么将会给攻击者带来非常大的难度;因为在页面上构造一个 a 标签 href 属性或者 img 标签 src 属性来构造一个请求是非常容易的,但是如果要 POST 提交,就必须要通过脚本才可以实现。

用CSRF token验证请求

因为 Midway 不涉及到淘宝分布式 session 及 token 校验这一层面逻辑,所以在 Midway 框架中,只将 token 在 server 和客户端之间进行转发,本身不做实际的校验工作。流程如下:

后续:在 Midway 中,Node.js 和淘宝的分布式 session 对接后,可以考虑在 Midway 这一层自动进行 token 校验;毕竟安全校验越早进行,成本也会更低。

建议:在 Midway 中,可以判断是否 request 中有 token 的值,如果一个修改操作,没有 token,可以直接在 Midway 层认为是不安全的,将请求丢弃掉。

其他安全问题

关于常见的 Web 安全问题,还有如下几种,这里只做一些简介,后续会持续继承到 Midway framework 中。

  • HTTP Headers安全
  • CRLF Injection 攻击者想办法在响应头中注入两个 CRLF 特殊字符,导致响应数据格式异常,从而注入 script 等
  • 拒绝访问攻击 每个请求因为都会默认带上 cookie,而服务器一般都会限制 cookie 的大小,这就导致了,如果用户客户端 cookie 被设置成了超过某个阀值,那么用户就再也无法访问网站了
  • cookie 防窃取 一般 cookie 窃取都是通过 JavaScript(XSS 漏洞) 获取到的,所以尽量将 cookie 设置成 http only,并且加上 cookie 过期时间

关于 cookie 的安全问题,之前 Webx 已经有较好的解决方案;此次 Midway 不负责 cookie 的设置和校验等工作,只负责转发到 Webx 层面进行 check

关于Node.js

XSS 等注入性漏洞是所有漏洞中最容易被忽略,占互联网总攻击的70%以上;开发者编写 Node.js 代码时,要时刻提醒自己,永远不要相信用户的输入。

比如如下几个例子。

  • var mod = fs.readFileSync('path'); 如果 path 来源于用户输入,那么假设用户输入 /etc/password,则会读取到不应该读取的内容,造成密码泄漏风险
  • var result = eval(jsonVal); 一定要确保 jsonVal 是 JSON,而不是用户的输入
  • …… 其他可能包含用户输入的地方,一定要确认用户的输入是我们期望的值

总结

前后端分离模式下,可以让传统的前端开发人员开始编写后端代码,虽然从架构上讲,只负责模板这一层,但也会接触大量的后端代码;所以安全对于前端来说,这是一个不小的挑战。

题图:https://unsplash.com/photos/DUNrQdEuYws By @Lucien Huang