【egg】透过egg-security源码明白具体Web安全防御手段

879 阅读8分钟

输出此文的原因:

之前零零散散看过很多web安全方面的文章,也总结过前端网站攻击和防御方式,但是前端常用的基本上也就是cookie、session、认证机制,具体实现主要还是在后端,最近正好在学egg,发现了安全插件egg-security,然后花时间研究下它的使用方式及源码,这样更深入了解其原理,弥补其他攻击盲点,同时对以后理解一些技术点和跟其他开发人员沟通起来也很方便,考虑问题也能更加全面,使用起来也能得心应手

总结之后,我收获:

  • 懂得什么是CSP
  • js怎么底层实现过滤和插入csrf或者nonce等
  • 更深入了解常见的安全漏洞,及防御手段
  • 对整个web安全有个知识体系

提醒:框架的安全插件是默认开启的

一、web安全威胁

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

  • 当网站需要直接输出用户输入的结果时,请务必使用 helper.escape() 包裹起来,如在 egg-view-nunjucks 里面就覆盖掉了内置的 escape。
const str = '><script>alert("abc") </script><';
console.log(ctx.helper.escape(str));
// => &gt;&lt;script&gt;alert(&quot;abc&quot;) &lt;/script&gt;&lt;
  • 网站输出的内容会提供给 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>

阶段性总结:反射型xss主要防御手段是通过字符过滤,使用常见场景总结为以下3方面:

  1. 当网站需要直接输出用户输入的结果时,使用 helper.escape() 编码
  2. 网站输出的内容会提供给 JavaScript 来使用,使用 helper.sjs() 将所有非白名单字符转义为 \x 形式
  3. 在 JavaScript 中输出 json,使用helper.sjson(),将 value 的值中,所有非白名单字符转义为 \x 形式

2. 存储的 XSS 攻击:通过提交带有恶意脚本的内容存储在服务器上

  1. 将富文本(包含 HTML 代码的文本)当成变量直接在模版里面输出时,需要用到 helper.shtml() 来处理。 使用它可以输出 HTML 的 tag,同时执行 XSS 的过滤动作,过滤掉非法的脚本。
const value = `<a href="http://www.domain.com">google</a><script>evilcode…</script>`;

{{ helper.shtml(value) }}

// => <a href="http://www.domain.com">google</a>&lt;script&gt;evilcode…&lt;/script&gt;

shtml 在 xss 模块基础上增加了针对域名的过滤,可以看自定义规则默认规则

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

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

注意:

  1. shtml 使用了严格的白名单机制,除了过滤掉 XSS 风险的字符串外, 在默认规则外的 tag 和 attr 都会被过滤掉。所以,一定要注意 shtml 的适用场景,一般是针对来自用户的富文本输入,切忌滥用,功能既受到限制,又会影响服务端性能。 此类场景一般是论坛、评论系统等,即便是论坛等如果不支持 HTML 内容输入,也不要使用此 Helper,直接使用 escape 即可
  1. 它是一个非常复杂的安全处理过程,对服务器处理性能一定影响,如果不是输出 HTML,请勿使用。

3. JSONP XSS

我们都知道jsonp是基于JSON格式的为解决跨域请求资源而产生的解决方案,它的用法是利用script的可跨域请求,后面拼接callback参数,但是这个方法存在两种风险可能导致 XSS:

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

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

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

防御内容:

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

可定义配置:

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

4. 其他 XSS 的防范方式

默认开启,禁用 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

5. CSRF(Cross-site request forgery,也被称为 One Click Attack 或者 Session Riding,通常缩写为 CSRF 或者 XSRF,是一种对网站的恶意利用)防范

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

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

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

  1. 同步表单的 CSRF 校验

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

<form method="POST" action="/upload?_csrf={{ ctx.csrf | safe }}" 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 进行注入,对应用开发者无感知。

BREACH 攻击:

  1. AJAX 请求 在 CSRF 默认配置下,token 会被设置在 Cookie 中,在 AJAX 请求的时候,可以从 Cookie 中取到 token,放置到 query、body 或者 header 中发送给服务端。
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
    },
  },
};
  1. Session vs Cookie 存储 默认配置下,框架会将 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
      ignoreJSON: true, // 默认为 false,当设置为 true 时,将会放过所有 content-type 为 `application/json` 的请求
    },
  },
};

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

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

阶段性总结框架对CSRF的防御:

  • 表单验证,提交表单的时候将 CSRF token 提交上来
  • 默认配置下,token 会被设置在 Cookie 中,可以放置到 query、body 或者 header 中发送给服务端
  • 最好将 token 存放到 Session 中,可配置
  • 调用 rotateCsrfSecret 刷新用户的 CSRF token

从此框架了解到的几种方式,其实就是CSRF攻击的常用防范方案。

6. XST(Cross-Site Tracing)的防范

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

接着我们发 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,造成了很大的风险。

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

7. 钓鱼攻击的防范

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

url 钓鱼

服务端未对传入的跳转 url 变量进行检查和控制,可能导致可恶意构造任意一个恶意地址,诱导用户跳转到恶意网站,可能这样钓鱼:

  • 通过转到恶意网站欺骗用户输入用户名和密码盗取用户信息
  • 欺骗用户进行金钱交易
  • 可能引发的 XSS 漏洞(主要是跳转常常使用 302 跳转,即设置 HTTP 响应头Locatioin: url,如果 url 包含了 CRLF,则会导致XSS)

防范方式:

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

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

  • ctx.redirect(url) 如果不在配置的白名单内,则禁止。
  • ctx.unsafeRedirect(url) 一般不建议使用,明确了解可能带来的风险后使用。
  • 安全方案覆盖了默认的ctx.redirect方法,所有的跳转均会经过安全域名的判断。

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

exports.security = {
  domainWhiteList:['.domain.com'],  // 安全白名单,以 . 开头
};
  • 若用户没有配置 domainWhiteList 或者 domainWhiteList数组内为空,则默认会对所有跳转请求放行,即等同于ctx.unsafeRedirect(url)

图片钓鱼

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

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

防范方式:

  • 框架提供了 .surl() 宏做 url 过滤
<a href="helper.surl($value)" />

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

iframe 钓鱼

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

防范方式:

  • 框架提供了 X-Frame-Options 安全头。默认值为 SAMEORIGIN,只允许同域把本页面当作 iframe 嵌入。当需要嵌入一些可信的第三方网时,可以关闭这个配置。

8. HPP(Http Parameter Pollution即 HTTP 参数污染攻击) 的防范

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

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

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

防范方式:

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

9. 中间人攻击与 HTTP / HTTPS

中间人(浏览器、路由器厂商、WIFI提供商、通信运营商等,如果使用了代理、翻墙软件则会引入更多中间人)可以对 HTTP 请求进行监控、劫持、阻挡。

可能产生的安全威胁有:

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

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

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

防范方式:

  • 使用 Egg 框架开发网站的开发者,务必推动自己的网站升级到 HTTPS

  • 框架默认关闭了 hsts Strict-Transport-Security。使得 HTTPS 站点不跳转到 HTTP,如果站点支持 HTTPS,请一定要开启

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

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

10. SSRF(Server-Side Request Forgery)的防范

SSRF安全漏洞常见于:开发者在服务端直接请求客户端传递进来的 URL 资源,一旦攻击者传入一些内部的 URL 即可发起 SSRF 攻击。

防范方式:

  • 基于内网 IP 黑名单的形式来防范 SSRF 攻击,通过对解析域名后得到的 IP 做过滤,禁止访问内部 IP 地址

使用方法:

框架在 ctx, app 和 agent 上都提供了 safeCurl 方法,在发起网络请求的同时会对指定的内网 IP 地址过滤,除此之外,该方法和框架提供的 curl 方法一致。

ctx.safeCurl(url, options)
app.safeCurl(url, options)
agent.safeCurl(url, options)

直接调用 safeCurl 方法其实并没有任何作用,还需要配合安全配置项。

  • ipBlackList(Array) - 配置内网 IP 名单,在这些网段内的 IP 地址无法被访问。
  • checkAddress(Function) - 直接配置一个检查 IP 地址的函数,根据函数的返回值来判断是否允许在 safeCurl 中被访问,当返回非 true 时,该 IP 无法被访问。checkAddress 优先级高于 ipBlackList。
// config/config.default.js
exports.security = {
  ssrf: {
    ipBlackList: [
      '10.0.0.0/8', // 支持 IP 网段
      '0.0.0.0/32',
      '127.0.0.1',  // 支持指定 IP 地址
    ],
    // 配置了 checkAddress 时,ipBlackList 不会生效
    checkAddress(ip) {
      return ip !== '127.0.0.1';
    },
  },
};

11. 其他安全工具

  • ctx.isSafeDomain(domain) 是否为安全域名。安全域名在配置中配置,见 ctx.redirect 部分。

  • app.injectCsrf(str) 提供了模板预处理-自动插入 CSRF key 的能力,可以自动在所有的 form 标签中插入 CSRF 隐藏域,用户就不需要手动写了。

const INPUT_CSRF = '\r\n<input type="hidden" name="_csrf" value="{{ctx.csrf}}" /></form>';

exports.injectCsrf = function injectCsrf(tmplStr) {
  tmplStr = tmplStr.replace(/(<form.*?>)([\s\S]*?)<\/form>/gi, function replaceCsrf(_, $1, $2) {
    const match = $2;
    if (match.indexOf('name="_csrf"') !== -1 || match.indexOf('name=\'_csrf\'') !== -1) {
      return $1 + match + '</form>';
    }
    return $1 + match + INPUT_CSRF;
  });

  return tmplStr;
};
  • app.injectNonce(str) 提供了模板预处理-会扫描模板中的 script 标签,并自动加上 nonce 头,如果网站开启了 CSP 安全头,并且想使用 CSP 2.0 nonce 特性,可以使用这个函数。
exports.injectNonce = function injectNonce(tmplStr) {
  tmplStr = tmplStr.replace(/<script(.*?)>([\s\S]*?)<\/script>/gi, function replaceNonce(_, $1, $2) {
    if ($1.indexOf('nonce=') === -1) {
      $1 += ' nonce="{{ctx.nonce}}"';
    }

    return '<script' + $1 + '>' + $2 + '</script>';
  });
  return tmplStr;
};
  • app.injectHijackingDefense(str) 对于没有开启 HTTPS 的网站,这个函数可以有效的防止运营商劫持。
const INJECTION_DEFENSE = '<!--for injection--><!--</html>--><!--for injection-->';

exports.injectHijackingDefense = function injectHijackingDefense(tmplStr) {
  return INJECTION_DEFENSE + tmplStr + INJECTION_DEFENSE;
};

二、源码分析

xframe

默认是SAMEORIGIN

module.exports = options => {
  return async function xframe(ctx, next) {
    await next();

    const opts = utils.merge(options, ctx.securityOptions.xframe);
    if (utils.checkIfIgnore(opts, ctx)) return;

    // DENY,SAMEORIGIN,ALLOW-FROM
    // https://developer.mozilla.org/en-US/docs/HTTP/X-Frame-Options?redirectlocale=en-US&redirectslug=The_X-FRAME-OPTIONS_response_header
    const value = opts.value || 'SAMEORIGIN';

    ctx.set('x-frame-options', value);
  };
};

csp

const HEADER = [
  'x-content-security-policy',
  'content-security-policy',
];
const REPORT_ONLY_HEADER = [
  'x-content-security-policy-report-only',
  'content-security-policy-report-only',
];

module.exports = options => {
  return async function csp(ctx, next) {
    await next();

    const opts = utils.merge(options, ctx.securityOptions.csp);
    if (utils.checkIfIgnore(opts, ctx)) return;

    let finalHeader;
    let value;
    const matchedOption = extend(true, {}, opts.policy);
    const isIE = platform.parse(ctx.header['user-agent']).name === 'IE';
    const bufArray = [];

    const headers = opts.reportOnly ? REPORT_ONLY_HEADER : HEADER;
    if (isIE && opts.supportIE) {
      finalHeader = headers[0];
    } else {
      finalHeader = headers[1];
    }

    for (const key in matchedOption) {

      value = matchedOption[key];
      value = Array.isArray(value) ? value : [ value ];

      // Other arrays are splitted into strings EXCEPT `sandbox`
      if (key === 'sandbox' && value[0] === true) {
        bufArray.push(key);
      } else {
        if (key === 'script-src') {
          const hasNonce = value.some(function(val) {
            return val.indexOf('nonce-') !== -1;
          });

          if (!hasNonce) {
            value.push('\'nonce-' + ctx.nonce + '\'');
          }
        }

        value = value.map(function(d) {
          if (d.startsWith('.')) {
            d = '*' + d;
          }
          return d;
        });
        bufArray.push(key + ' ' + value.join(' '));
      }
    }
    const headerString = bufArray.join(';');
    ctx.set(finalHeader, headerString);
    ctx.set('x-csp-nonce', ctx.nonce);
  };
};

csrf

function csrf(ctx, next) {
    if (utils.checkIfIgnore(options, ctx)) {
      return next();
    }
    // ensure csrf token exists
    if ([ 'any', 'all', 'ctoken' ].includes(options.type)) {
      ctx.ensureCsrfSecret();
    }
    // ignore requests: get, head, options and trace  忽略这些方法请求
    const method = ctx.method;
    if (method === 'GET' ||
      method === 'HEAD' ||
      method === 'OPTIONS' ||
      method === 'TRACE') {
      return next();
    }
	//忽略json
    if (options.ignoreJSON && typeis.is(ctx.get('content-type'), 'json')) {
      return next();
    }

    const body = ctx.request.body || {};
    debug('%s %s, got %j', ctx.method, ctx.url, body);
    ctx.assertCsrf();
    return next();
  };

dta(Directory_traversal_attack,目录遍历攻击)

en.wikipedia.org/wiki/Direct…

function dta(ctx, next) {
  const path = ctx.path;
  if (!isSafePath(path, ctx)) {
    ctx.throw(400);
  }
  return next();
};

hsts(HTTP Strict Transport Security,HTTP严格传输安全协议)

设置strict-transport-security头

async function hsts(ctx, next) {
    await next();
    const opts = utils.merge(options, ctx.securityOptions.hsts);
    if (utils.checkIfIgnore(opts, ctx)) return;

    let val = 'max-age=' + opts.maxAge;
    // If opts.includeSubdomains is defined,the rule is also valid for all the sub domains of the website
    if (opts.includeSubdomains) {
      val += '; includeSubdomains';
    }
    ctx.set('strict-transport-security', val);
 };

methodnoallow

const methods = require('methods');
const METHODS_NOT_ALLOWED = [ 'trace', 'track' ];
const safeHttpMethodsMap = {};

for (const method of methods) {
  if (!METHODS_NOT_ALLOWED.includes(method)) {
    safeHttpMethodsMap[method.toUpperCase()] = true;
  }
}

// https://www.owasp.org/index.php/Cross_Site_Tracing
// http://jsperf.com/find-by-map-with-find-by-array
module.exports = () => {
  return function notAllow(ctx, next) {
    // ctx.method is upper case
    if (!safeHttpMethodsMap[ctx.method]) {
      ctx.throw(405);
    }
    return next();
  };
};

noopen

// @see http://blogs.msdn.com/b/ieinternals/archive/2009/06/30/internet-explorer-custom-http-headers.aspx
module.exports = options => {
  return async function noopen(ctx, next) {
    await next();

    const opts = utils.merge(options, ctx.securityOptions.noopen);
    if (utils.checkIfIgnore(opts, ctx)) return;

    ctx.set('x-download-options', 'noopen');
  };
};

nosniff

const statuses = require('statuses');
const utils = require('../utils');

module.exports = options => {
  return async function nosniff(ctx, next) {
    await next();

    // ignore redirect response
    if (statuses.redirect[ctx.status]) return;

    const opts = utils.merge(options, ctx.securityOptions.nosniff);
    if (utils.checkIfIgnore(opts, ctx)) return;

    ctx.set('x-content-type-options', 'nosniff');
  };
};

referrerPolicy

// https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Referrer-Policy
const ALLOWED_POLICIES_ENUM = [
  'no-referrer',
  'no-referrer-when-downgrade',
  'origin',
  'origin-when-cross-origin',
  'same-origin',
  'strict-origin',
  'strict-origin-when-cross-origin',
  'unsafe-url',
  '',
];

module.exports = options => {
  return async function referrerPolicy(ctx, next) {
    await next();

    const opts = utils.merge(options, ctx.securityOptions.refererPolicy);
    if (utils.checkIfIgnore(opts, ctx)) { return; }
    const policy = opts.value;
    if (!ALLOWED_POLICIES_ENUM.includes(policy)) {
      throw new Error('"' + policy + '" is not available."');
    }

    ctx.set('referrer-policy', policy);
  };
};

xssProtection

module.exports = options => {
  return async function xssProtection(ctx, next) {
    await next();

    const opts = utils.merge(options, ctx.securityOptions.xssProtection);
    if (utils.checkIfIgnore(opts, ctx)) return;

    ctx.set('x-xss-protection', opts.value);
  };
};

三、 总结

总结了以下几种安全威胁:

1. 反射型XSS(JSONPXSS)

  • 当网站需要直接输出用户输入的结果时,务必使用 helper.escape() 编码,如在egg-view-bunjunks里面会覆盖掉内置的escape
  • 网站输出的内容会提供给 JavaScript 来使用,使用 helper.sjs() 将所有非白名单字符转义为 \x 形式
  • 在 JavaScript 中输出 json,使用helper.sjson(),将 value 的值中,所有非白名单字符转义为 \x 形式

2. 存储型XSS

  • 将富文本(包含 HTML 代码的文本)当成变量直接在模版里面输出时,需要用到 helper.shtml() 来处理

3. JSONP XSS

  • callback 函数名词最长 50 个字符限制
  • callback 函数名只允许 [, ], a-zA-Z0123456789_, $, .,防止一般的 XSS,utf-7 XSS等攻击。 4. 其他 XSS 的防范方式
  • CSP
  • X-Download-Options:noopen
  • X-Content-Type-Options:nosniff
  • XSS-Protection

5. CSRF

  • 表单验证,提交表单的时候将 CSRF token 提交上来
  • 默认配置下,token 会被设置在 Cookie 中,可以放置到 query、body 或者 header 中发送给服务端
  • 最好将 token 存放到 Session 中,可配置
  • 调用 rotateCsrfSecret 刷新用户的 CSRF token 6. XST(Cross-Site Tracing)
  • 框架已经禁止了 trace,track,options 三种危险类型请求。 7. 钓鱼(url钓鱼、图片钓鱼、iframe钓鱼)
  • url钓鱼
    • ctx.redirect(url) 配置白名单。
    • ctx.unsafeRedirect(url) 一般不建议使用,明确了解可能带来的风险后使用。
  • 图片钓鱼:框架提供了 .surl() 宏做 url 过滤
  • iframe钓鱼:框架提供了 X-Frame-Options 安全头,默认值为 SAMEORIGIN,只允许同域把本页面当作 iframe 嵌入。当需要嵌入一些可信的第三方网时,可以关闭这个配置

8. HPP(Http Parameter Pollution即 HTTP 参数污染攻击)

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

9. 中间人攻击与 HTTP / HTTPS

  • 使用 Egg 框架开发网站的开发者,务必推动自己的网站升级到 HTTPS

  • 框架默认关闭了 hsts Strict-Transport-Security。使得 HTTPS 站点不跳转到 HTTP,如果站点支持 HTTPS,请一定要开启。

  • 如果我们的Web 站点是 http 站点,需要关闭这个头

10. SSRF(Server-Side Request Forgery)

  • 基于内网 IP 黑名单的形式来防范 SSRF 攻击,通过对解析域名后得到的 IP 做过滤,禁止访问内部 IP 地址

11. 其他安全工具

  • ctx.isSafeDomain(domain)
  • app.injectCsrf(str)
  • app.injectNonce(str)
  • app.injectHijackingDefense(str)

总结了egg-security框架对于以上威胁的安全防御的使用方法,也列举了其他常用安全方法

同时分析框架中防御手段的源码实现

总结此框架内置丰富的解决方案:

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

参考:

github.com/eggjs/egg-s…

github.com/eggjs/egg/b…