Пару месяцев назад я начал заниматься проектом под названием malicious-packages (ака "вредоносные пакеты"). Он следит за обновлениями в npm репозитории, скачивает все новые модули, а затем проверяет их на вшивость — ищет сетевую активность, подозрительные операции с файловой системой и т.д. Даже маленькие проекты на node.js часто имеют большое дерево зависимостей, и у разработчиков физически нет возможности проверить их все. Это даёт злоумышленникам огромный простор для манёвра, и возникает вопрос — сколько же всякой гадости прячется по тёмным углам npm registry? 180000 проверенных пакетов спустя я получил примерный ответ.
И этот ответ — пожалуй, не так уж и много.
[прим.: на medium есть англоязычная версия этой статьи, также за моим авторством]
Что npm пакет может сделать c вашей системой?
У пакета есть два основных пути навредить вам — при установке / удалении и в момент запуска вашего приложения. Давайте рассмотрим оба варианта на примерах.
NPM скрипты позволяют пакетам выполнять произвольные команды в момент установки и удаления. К ним относятся preinstall
, install
, postinstall
, preuninstall
и postuninstall
, которые автоматически выполняются npm в соответствующий момент жизненного цикла пакета. Что они могут сделать? Всё то же самое, что может сделать ваш текущий пользователь — например удалить все ваши фотографии с последнего отпуска, или слить историю вашего браузера в ФБР (хотя, скорее всего, она и так у них есть). Это поведение можно отключить передав флаг --ignore-scripts
, но, во-первых, это никто не делает, а во-вторых — таким образом можно сломать кучу вполне себе благонадёжных пакетов. Именно через скрипты была осуществлена нашумевшая атака на ESLint, которая затронула пользователей eslint-scope (6 миллионов установок в неделю) и eslint-config-eslint (2 тысячи установок в неделю).
Пакет получает второй шанс усложнить вам жизнь при инициализации (обычно происходит при первом вызове require
). Теперь у него появляется возможность модифицировать глобальные переменные и другие пакеты, чтобы, например, украсть приватный ключ от вашего биткоин-кошелька, или сделать метод crypto.randomBytes
не таким уж и случайным.
Сколько вредоносных пакетов было обнаружено?
Что ж, список нельзя назвать впечатляющим, всего было найдено 3 пакета, которые к настоящему моменту удалены из npm репозитория стараниями npm security team. Давайте пройдёмся по нему:
- commander-js пытается маскироваться под настоящий
commander.js
(который https://www.npmjs.com/package/commander), но содержит одну необычную деталь, а именноpostinstall
скрипт, скачивающий и выполняющий содержимое http://23.94.46.191/update.json (на момент публикации не содержит ничего криминального). Security advisory: https://www.npmjs.com/advisories/763. - rrgod через всё те же скрипты загружает и выполняет скрипт из http://static.ricterz.me, который, в свою очередь, пытается загрузить ещё один скрипт, в настоящее время недоступный. Security advisory: https://www.npmjs.com/advisories/764.
- portionfatty12 нельзя назвать полностью вредоносным, однако способ, которым автор собирает статистику об установках своего детища весьма сомнителен — это отправка вашего публичного ssh-ключа (
~/.ssh/id_rsa.pub
) на сервер, который в настоящий момент уже недоступен. Security advisory: https://www.npmjs.com/advisories/765.
Несмотря на весьма скромные результаты, в процессе анализа всех подозрительных пакетов (а я просмотрел более 3000 отчётов, чтобы найти эти три жемчужины) обнаружилось немало забавных и не очень вещей, о которых вы обычно не задумываетесь (или тщательно стараетесь не думать) набирая npm install
. Итак, давайте представим, что вы случайно выбрали один из множества пакетов из репозитория и устанавливаете его. Что может пойти не так?
1. Пакет может переопределять методы в других пакетах (в том числе и из стандартной поставки Node.js)
Впрочем, если вы прочитали пару предыдущих абзацев или работаете с экосистемой javascript больше недели, то вряд ли для вас это будет являться новостью. А вот масштабы этой катастрофы вполне могли от вас ускользнуть. Так, если в вашем проекте есть хотя бы несколько зависимостей, то скорее всего метод fs.closeSync у вас уже переопределён (и, может быть, не один раз). Огромное количество пакетов модифицирует чужое API, но лишь немногие из них имеют хоть сколько-нибудь вескую причину для этого. Среди "рекордсменов" — graceful-fs с 12 миллионами установок в неделю, который переопределяет десяток методов из fs. Так же стоит отметить async-listener, переопределяющий 46 различных методов, включая злополучный crypto.randomBytes
, что меня несколько напрягло, когда я впервые это обнаружил.
Представьте, каково будет искать баг, вызванный таким переопределением из какой-нибудь зависимости в глубине иерархии. Впрочем, причин для переживаний нет, потому что...
2. Пакет может определять изменённое API вносить в него дополнительные исправления
Да, некоторые пакеты так и поступают (чаще всего в отношении того же graceful-fs
) используя чудеса акробатики вроде /graceful-fs/.test(fs.closeSync.toString())
. Так что, если вы вдруг столкнулись с непонятными проблемами в стандартной библиотеке, попробуйте просто установить парочку случайных npm пакетов. Или просто выключите компьютер и прогуляйтесь по ближайшему парку, жизнь слишком коротка, чтобы разбираться во всём этом.
3. Он может отправлять аналитику
Adblock вас не защитит, если дело происходит в консоли, и авторы некоторых пакетов успешно этим пользуются. Некоторые отправляют самую базовую информацию, как, например, ecdsa-csr:
// POST https://api.therootcompany.com/api/therootcompany.com/public/ping
{
"package":"ecdsa-csr",
"version":"1.1.1",
"node":"v10.14.2",
"arch":"x64",
"platform":"linux",
"release":"4.9.125-linuxkit",
"action":"install",
"ppid":"eDSeYr9XUNRi9WhWli5smBNAvdw="
}
Некоторые не так стесняются. Вот, например часть отчёта serverless (оригинал раза в 2 больше):
// POST https://tracking.serverlessteam.com/v1/track
{
"userId":"0e32cba0-14ef-11e9-9f89-b7ed4ca5dbba",
"event":"framework_stat",
"properties":{
"version":2,
"general":{
"userId":"0e32cba0-14ef-11e9-9f89-b7ed4ca5dbba",
"context":"install",
"timestamp":1547135257977,
"timezone":"GMT+0000",
"operatingSystem":"linux",
"userAgent":"cli",
"serverlessVersion":"1.35.1",
"nodeJsVersion":"v10.14.2",
"isDockerContainer":true,
"isCISystem":false,
"ciSystem":null
}
}
}
К счастью, jquery
никакой статистики не отправляет, так что его по-прежнему можно устанавливать в тайне ото всех. До поры до времени.
4. Ваш компьютер может использоваться вместо CI/CD сервера
Зачем вам компилировать ваш пакет, если это могут сделать ваши пользователи при установке? Можно получить дополнительную ачивку, если указать только мажорную версию требуемого компилятора, например typescript@3
. Одна надежда на грамотных ребят из Microsoft, которые умеют делать правильный semver.
Можно ли зайти ещё дальше? Конечно!
"postinstall": "eslint --ext .js,.vue --fix src"
Теперь можно спать спокойно — все пользователи получат идеально отформатированные исходники вашего пакета.
5. Он может попытаться напугать вас
Если вы посмотрели все серии Mr. Robot, но всё ещё недостаточно замотивированы, чтобы прочитать Computer Networks и сделать что-нибудь действительно впечатляющее, то есть простое решение — продемонстрируйте миру свои умения посредством пары строчек в postinstall
скрипте. Именно так и поступил автор pizza-pasta (а заодно подарил мне КДПВ).
{
"name": "pizza-pasta",
"author": "Zeavo",
"scripts": {
"install": "mkdir -p ~/Desktop/hacked && touch ~/Desktop/hacked/pwnddddd && wget https://imgur.com/download/KTDNt5I -P ~/Desktop/hacked/",
"postinstall": "find ~/.ssh | xargs cat || true && printf '\n\n\n\n\n\nOH HEY LOOK SSH KEYS\n\n\n\nHappy Birthday! Youve been h4ck0red\n\n\n'"
}
}
6. Пакет может загружать и исполнять bash-скрипты
Слышали ли вы, что делать curl|bash
не очень хорошая идея? Если нет, то вы вполне можете оказаться сотрудником ORESoftware, у которых есть целая куча пакетов с подобными строчками в postinstall
скрипте:
curl --silent -o- https://raw.githubusercontent.com/oresoftware/realpath/master/assets/install.sh | bash
Пока что в этом скрипте нет ничего криминального… Пока. Надеюсь, что все, у кого есть доступ к master
в oresoftware/realpath
, исключительно честные и порядочные люди.
7. У вас могут попросить пароль
Автор magicleap придумал достаточно необычный способ распространять приватный пакет через публичный репозиторий. Его проект состоит из зашифрованного архива и утилит для его расшифровки — но только если вы поместите правильный ключ в переменную окружения MAGICLEAP
:
// Оригинал: https://github.com/modulesio/magicleap/blob/master/decrypt.js
var key = process.env['MAGICLEAP'];
console.warn('Decrypting magicleap module with MAGICLEAP environment variable');
const ws = fs.createReadStream(path.join(__dirname, 'lib.zip.enc'))
.pipe(crypto.createDecipher('aes-256-cbc', Buffer.from(key, 'base64')))
.pipe(fs.createWriteStream(path.join(__dirname, 'lib.zip')));
8. Пакет может пропатчить сам себя во время установки
У автора fake-template нет времени, чтобы отделять тесты от непосредственного кода, тем более, что это легко сделать, добавив в конец тестовых строк специальный комментарий:
// Оригинал: https://github.com/framp/fake-template/blob/master/index.js
const template = (string, tag=defaultTag) => {
if (mode !== 'literal') throw new Error('Invalid template')
return (context={}) => tag(literals, ...expressions.map(evalInContext(context)))
}
assert.equal(template('')(), ``) // TEST
assert.equal(template('abc')(), `abc`) // TEST
const dog = 'Orlando' // TEST
assert.equal(template('abc ${dog} lol ${cat}')({dog}), `abc ${dog} lol ${cat}`) // TEST
А затем удалить их через sed
:
"postinstall": "sed -i '/\\/\\/ TEST/d' index.js"
Просто и элегантно!
9. Пакет может изменить настройки npm
На мой скромный взгляд, package-json.lock
— отличная штука, которая может спасти от целой кучи проблем, вызванных халатностью других разработчиков. Впрочем, у некоторых оппонентов этой идеи есть достаточно веские доводы против:
"preinstall": "npm config set package-lock false"
10. Он может изменить фон вашего рабочего стола на фото Николаса Кейджа
Вот ссылка, просто на всякий случай — https://www.npmjs.com/package/cage-js. Возможно, стоило и этот пакет зарепортить, как вредоносный.
11. Пакет может вас зарикроллить
Именно этим занимается ember-data-react, открывая знаменитое видео во время установки. К сожалению, никаких данных из Ember в React с его помощью перенести не удастся — в нем нет ни строчки javascript кода.
12. Он может просто не установиться
Несуществующие зависимости, неправильно указанные версии, приватные репозитории, канувшие в лету — вы не сможете установить около 0.6% всех пакетов из npm репозитория.
Вместо заключения
NPM пакеты могут делать странные вещи с вашей системой, и у вас не так много вариантов защиты от этого. Используйте package-lock.json
, чтобы избежать внезапных обновлений (и следите, чтобы никто его не отключил без вашего ведома), настройте CSP на фронтенде, чтобы бэкдор в стороннем модуле хотя бы не смог слить данные своему автору. И сделайте бэкап ваших фотографий, на всякий случай.
Если вы чувствуете в себе достаточно сил для погружения в чудесный мир npm пакетов — вы можете найти все исходники здесь: https://github.com/malicious-packages/core. Утилита полна хаков и неоптимальных решений, но она вполне справляется со своей задачей. Так же в репозитории есть MongoDB дамп с результатами анализа более 180 000 пакетов, главное, не забудьте добавить фильтр {'reports.status': 'unverified'}
. Я не планирую больше развивать этот проект из-за недостатка времени, но постараюсь помочь со всеми вопросами и проблемами, если таковые будут.
Берегите себя и свои приложения!