Пару месяцев назад я начал заниматься проектом под названием malicious-packages (ака "вредоносные пакеты"). Он следит за обновлениями в npm репозитории, скачивает все новые модули, а затем проверяет их на вшивость — ищет сетевую активность, подозрительные операции с файловой системой и т.д. Даже маленькие проекты на node.js часто имеют большое дерево зависимостей, и у разработчиков физически нет возможности проверить их все. Это даёт злоумышленникам огромный простор для манёвра, и возникает вопрос — сколько же всякой гадости прячется по тёмным углам npm registry? 180000 проверенных пакетов спустя я получил примерный ответ.


image


И этот ответ — пожалуй, не так уж и много.
[прим.: на medium есть англоязычная версия этой статьи, также за моим авторством]


Что npm пакет может сделать c вашей системой?


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


NPM скрипты позволяют пакетам выполнять произвольные команды в момент установки и удаления. К ним относятся preinstall, install, postinstall, preuninstall и postuninstall, которые автоматически выполняются npm в соответствующий момент жизненного цикла пакета. Что они могут сделать? Всё то же самое, что может сделать ваш текущий пользователь — например удалить все ваши фотографии с последнего отпуска, или слить историю вашего браузера в ФБР (хотя, скорее всего, она и так у них есть). Это поведение можно отключить передав флаг --ignore-scripts, но, во-первых, это никто не делает, а во-вторых — таким образом можно сломать кучу вполне себе благонадёжных пакетов. Именно через скрипты была осуществлена нашумевшая атака на ESLint, которая затронула пользователей eslint-scope (6 миллионов установок в неделю) и eslint-config-eslint (2 тысячи установок в неделю).


Пакет получает второй шанс усложнить вам жизнь при инициализации (обычно происходит при первом вызове require). Теперь у него появляется возможность модифицировать глобальные переменные и другие пакеты, чтобы, например, украсть приватный ключ от вашего биткоин-кошелька, или сделать метод crypto.randomBytes не таким уж и случайным.


image


Сколько вредоносных пакетов было обнаружено?


Что ж, список нельзя назвать впечатляющим, всего было найдено 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 вносить в него дополнительные исправления


image


Да, некоторые пакеты так и поступают (чаще всего в отношении того же 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. Он может попытаться напугать вас


image


Если вы посмотрели все серии 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. Он может изменить фон вашего рабочего стола на фото Николаса Кейджа


image


Вот ссылка, просто на всякий случай — https://www.npmjs.com/package/cage-js. Возможно, стоило и этот пакет зарепортить, как вредоносный.


11. Пакет может вас зарикроллить


Именно этим занимается ember-data-react, открывая знаменитое видео во время установки. К сожалению, никаких данных из Ember в React с его помощью перенести не удастся — в нем нет ни строчки javascript кода.


12. Он может просто не установиться


image
Несуществующие зависимости, неправильно указанные версии, приватные репозитории, канувшие в лету — вы не сможете установить около 0.6% всех пакетов из npm репозитория.


Вместо заключения


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


Если вы чувствуете в себе достаточно сил для погружения в чудесный мир npm пакетов — вы можете найти все исходники здесь: https://github.com/malicious-packages/core. Утилита полна хаков и неоптимальных решений, но она вполне справляется со своей задачей. Так же в репозитории есть MongoDB дамп с результатами анализа более 180 000 пакетов, главное, не забудьте добавить фильтр {'reports.status': 'unverified'}. Я не планирую больше развивать этот проект из-за недостатка времени, но постараюсь помочь со всеми вопросами и проблемами, если таковые будут.


Берегите себя и свои приложения!

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


  1. index0h
    15.01.2019 13:01

    Иногда кажется, что одно из предназначений экосистемы NodeJS/NPM — это пособие для изучения ИБ))


    1. stavinsky
      15.01.2019 14:24

      Ну python pip тоже веселый.
      pypi.org/project/russian_roulette
      (Не устанавливать. Вот почему: github.com/charlesreid1/dont-sudo-pip/blob/source/setup.py )


      1. v1000
        15.01.2019 23:01

        Так он безобидный вроде:)
        Но код почитаешь — пугает. Пусть и закомментарено все что нужно — но все равно пугает. Особенно порадовали сообщения в консоль:)


  1. Zoolander
    15.01.2019 18:07

    закрадывается мысль делать npm install и npm uninstall на специальной виртуальной машине, которая создается с доступом только к определенным папкам

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


    1. theaklair
      15.01.2019 19:25

      docker ftw


    1. akimdi
      15.01.2019 22:55
      +1

      пробовали Qubes OS? там это всё по-умолчанию.


      1. Zoolander
        16.01.2019 06:44

        очень интересный проект, мечта параноика )


    1. Keyten
      16.01.2019 11:57

      Кста-ати в докере так и делают, делать установку в одном образе и переносить всё установленное в другой — не редкость.


  1. buldo
    15.01.2019 19:35

    С другой стороны всё распространяется исходниками и в теории можно накручивать автоматику с проверками. А в nuget распространяют бинари, которым надо доверять. Хорошо хоть отказались от скриптов в новых версиях


    1. iig
      15.01.2019 23:40

      Исходники это конечно хорошо. Но ведь и язык прямо создан для обфускации.


  1. justboris
    16.01.2019 00:34

    У автора fake-template нет времени, чтобы отделять тесты от непосредственного кода

    Видимо, автор любит Rust, в котором это распространенная практика.


  1. Elfet
    16.01.2019 09:19
    +1

    Несмотря на всё это npm отлично справляется. Есть система безопасности, пакеты мониторятся.


    Все пакеты найденные в статье имеют 0 а то и меньше установок. Учитывая популярность npm не стоит этому удивляться. Конечно бывают и большие хаки (как с eslint) и они являются желаймой целью хакеров.


    Думаю централизованность npm тут выигрывает у например децентрализованого go. Кто будет проверять все сущ репы? А если популярная репа окажется хакнута?


    1. index0h
      16.01.2019 10:30

      Думаю централизованность npm тут выигрывает у например децентрализованого go. Кто будет проверять все сущ репы? А если популярная репа окажется хакнута?

      В чем же выигрывает?


      • В том, что есть какие-то парни в npm, на которых в случае взлома можно спихнуть вину?
      • В том, что твоим пакетом управляет какая-то левая компания? Вспомните историю с leftpad.
      • В том, что пакет != код из репозитория, и уязвимости как с тем же eslint в принципе возможны?

      Кто будет проверять все сущ репы?

      Это задача инженера, который подключает пакет.


      А если популярная репа окажется хакнута?

      В полной мере от этого никто не застрахован. У того же Go и в случае mod и в случае dep отсутствуют preisntall, postinstall и вот это вот все, что закрывает довольно большой источник уязвимостей.


      1. Elfet
        16.01.2019 13:59
        +1

        В том, что есть какие-то парни в npm, на которых в случае взлома можно спихнуть вину?

        Это плюс npm что есть хотя бы кто-то кто проверяет и дорожит своей репутацией.


        В том, что твоим пакетом управляет какая-то левая компания? Вспомните историю с leftpad.

        Вспомнил. Что дальше? Из-за того что есть npm пакет бытро вернули, убрали возможность удалять пакеты.


        А что было бы если кто-то удалить свою Go репу? А если она была бы такая же популярная как left-pad? Думаю прошло бы приличное количество времени прежде чем все зависимые репы обновились бы и сколько всё было бы сломано никто не знает.


        История с left-pad стала такой знаменитой благодоря размером npm.


        Это задача инженера, который подключает пакет.

        Это справедливо как для npm, так и для go.


        В полной мере от этого никто не застрахован. У того же Go и в случае mod и в случае dep отсутствуют preisntall, postinstall и вот это вот все, что закрывает довольно большой источник уязвимостей.

        Тут согласен, это минус npm.


        1. index0h
          16.01.2019 15:56
          -1

          Это плюс npm что есть хотя бы кто-то кто проверяет и дорожит своей репутацией.

          Ок, вот смотрите: есть github на нем есть проект MyVendor\MyProject. Проект дико популярен, все здорово, его аудитом, пусть и косвенно занимаются люди, которые его используют. То, что я вижу в репозитории и то, что загрузится в зависимости — одно и то же.


          Теперь берем npm. Так же с пакетом MyProject. Как я могу посмотреть код пакета? Только установив его через npm. Потому, что содержимое пакета и содержимое репозитория — это разные вещи.


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


          Теперь вопрос, а за что они (npm) отвечают, особенно с учетом того, что большинство пакетов под MIT/BSD/WTFPL?


          Вспомнил. Что дальше? Из-за того что есть npm пакет бытро вернули, убрали возможность удалять пакеты.

          Не вспомнили. Причина удаления leftpad автором заключается в том, что другой пакет (kik) этого же автора администрация npm отозвала, поставив перед фактом, нарушая собственные правила.


          1. mayorovp
            16.01.2019 16:26

            Ну так представьте что тот же github точно так же удалит чужой репозиторий из-за юридических проблем, а автор обидится и удалит вообще все свои репозитории…