TL; DR


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



Под катом описывается proof-of-concept библиотеки, реализующей механизм загрузки npm-модулей с возможностью установить права подобно тому, как на Android можно выдавать приложению конкретные разрешения.

Вместо

var lib = require('untrusted-lib');

предлагается писать где-нибудь

var paraquire = require('paraquire')(module);

и затем

var lib = paraquire('untrusted-lib');

или же

var lib = paraquire('untrusted-lib', {builtin:{https:true}});

Исходный код доступен на гитхабе под LGPLv3.

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

Краткий анализ истории


В настоящее время известно несколько векторов атаки на npm и его пакеты. Для атаки наиболее привлекательны те пакеты, от которых зависят многие другие пакеты.

  1. Отзыв пакета. В разрушительном виде продемонстрирован Азером Кочулу, отозвавшим пакет leftpad. В настоящее время неосуществим, т.к. отзыв пакетов очень сильно ограничен.
  2. Вредоносные preinstall- и postinstall-скрипты. В настоящее время — основной способ атаки (см. ссылку выше). Теоретически может быть легко пресечён флагом npm install, который бы приводил к выдаче предупреждения перед запуском любого shell-скрипта, чтобы уже сам программист мог решить, запускать ли предложенное. Одно дело, когда preinstall-скрипт пытается запустить PhantomJS (headless-браузер), и совсем другое — когда этого требует маленькая библиотечка для добавления пробелов слева к строке. Почему такой флаг до сих пор не введён — неясно.
    UPD: Спасибо SDSWanderer за комментарий о возможности сделать

    npm install --ignore-scripts
    

    Впрочем, интерактивный вариант (желательно с показом содержимого скрипта) был бы тоже не лишним.
  3. И, наконец, можно прятать вредоносную нагрузку непосредственно в исполняемый код пакета, как в шуточной форме описано здесь. Команда npm shrinkwrap, жёстко фиксирующая версию пакета, не поможет от «мин замедленного действия», которые, например, ждут определённой даты (вспомним «Чернобыль»). Кроме того, при нынешнем обилии зависимостей провести аудит исходного кода даже одной версии нужного пакета вместе со всем, что этот пакет использует — задача, конечно, посильная, но явно не повседневная.

Именно для борьбы с последним вектором и создана библиотека paraquire. Код недоверенных библиотек изолируется в отдельные контексты исполнения. Активно используются внутренние средства самой NodeJS для управления модулями. Иначе говоря, создана альтернативная система управления модулями, на возможность чего недвусмысленно указывают сами разработчики NodeJS.

Использование


В настоящее время API весьма небогато.

Итак, для начала загрузим саму библиотеку paraquire где-нибудь в относительно доверенном коде, т.е. коде нашего приложения:

var paraquire = require('paraquire')(module);

Обратите внимание на "(module)". Так как paraquire занимается управлением модулями, ему требуется знать, к какому именно модулю придётся подключать зависимости, поэтому подключение paraquire выглядит несколько необычно. Вы можете подключить paraquire к любому количеству модулей из вашего проекта.

Подключим теперь зависимости. Например, пусть untrusted-lib — библиотека, которой, по идее, ничего требоваться не должно (например, leftpad или imurmurhash). Тогда мы можем вовсе не давать ей доступ ни к чему, подключив её так:

var lib = paraquire('untrusted-lib');

Всё, попытка сделать require('fs') в коде untrusted-lib вызовет ошибку.

У функции paraquire есть и второй, опциональный, параметр-объект. Например, вот так можно предоставить библиотеке untrusted-lib доступ к встроенным модулям http и https, а также к консоли console и process.argv:

var lib = paraquire('untrusted-lib', {
    builtin: {
        http: true,
        https: true,
    },
    sandbox: {
        console: console,
        process: {argv:process.argv}
    },
});

Кстати, давать доступ ко всему process настоятельно не рекомендуется, в частности, из-за process.env и process.bindings('fs').

Прошу совета


Как уже было сказано выше, я не являюсь достаточно опытным разработчиком, и потому эта краткая публикация имеет своей целью скорее инициировать обсуждение. Актуальна ли эта тема? Как подобные проблемы решаются в других языках, в частности, в Ruby и Python? Может быть, кто-то может предложить способ обойти paraquire и получить повышенные привилегии?

В каком направлении следует развивать API? Может быть, стоит проконсультироваться у кого-то из зарубежных мэтров? Какие вопросы осветить в дальнейших статьях и стОит ли их вообще писать?

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


  1. SDSWanderer
    24.08.2017 23:23
    +8

    Почему такой флаг до сих пор не введён — неясно.

    npm install --ignore-scripts


    Как подобные проблемы решаются в других языках, в частности, в Ruby и Python?

    Насколько я знаю — да никак. Мир не идеален, дерьмо случается, но в данном случае не так уж часто. Честно говоря и ни разу не столкнулся с вредоносным модулем на практике, хотя уже лет 5 как использую npm. Повышенное внимание к npm в этом контексте обусловленно в первую очередь, его популярностью. Тем не меннее, считаю что автор предложил неплохое решение.


    1. NickKolok Автор
      25.08.2017 11:07

      Спасибо за наводку, добавил в статью.


  1. k12th
    25.08.2017 02:14

    Может быть вместо этого проксировать встроенные модули, в хэндлере try {throw} catch, по стэку смотреть, откуда была вызвана функция и либо разрешать и делать что попрошено, либо пробрасывать исключение наверх… Хотя не, производительность просядет, наверное...


    1. vintage
      25.08.2017 08:56
      +3

      Ошибку не обязательно кидать, чтобы получить из неё стек.


      new Error('stack trace').stack

      Но взятие стека на каждый чих — весьма не быстро, да.


  1. Alternator
    25.08.2017 02:41

    Размазывать права по всему коду приложения?
    Более того если один внешний модуль подключается их двух точек вашего приложения? — Что произойдет если они укажут разные права?
    Вариантом получше видится вызов библиотеки в одной точке приложения, и там же определить права для всех внешних модулей(ну или вообще в отдельном конфиг-файле)


    1. NickKolok Автор
      25.08.2017 09:38
      +1

      Модуль загрузится с разными правами. Компиляция — один раз, запуск в контекстах — в двух разных. Вот, в тестах есть: github.com/nickkolok/paraquire/blob/master/tests/manage-builtin-fs/main.js


  1. arvitaly
    25.08.2017 05:27
    +3

    Ни в коем случае нельзя решать этот вопрос изменением исходных кодов, еще один уровень мета-программирования JS не выдержит.
    Решение такое: подменяем require, это не проблема, в отдельном конфиг-файле указываем какие модули мокать, и какие у них права, тут можно создать группы прав.
    В идеале, требуемые права должны быть в package.json самого пакета, как у приложений в мобильных сторах.

    Еще вариант, уровень доверия пакета определять на основе его использования в крупных репозиториях, а уж крупные компании могут и должны позволить себе аудит безопасности.


    1. Hardcoin
      25.08.2017 10:11
      +3

      В идеале, требуемые права должны быть в package.json самого пакета

      Разумеется. Потенциальный злоумышленник пусть сам указывает, какие права ему нужны.


      1. NickKolok Автор
        25.08.2017 10:50

        А мы сможем посмотреть и решить, дать или не дать :)


      1. vtvz_ru
        25.08.2017 13:16

        Как по мне, это хорошее решение. Просто потому, что разработчикам пакетов придется оправдываться перед пользователями, зачем им какое-либо разрешение. Если пакет добавляет пробелы в начало строки, а требует доступ в сеть и к файловой системе, то либо он постарается объяснить, зачем ему это, либо те, кого беспокоит безопасность его приложения, найдут другой пакет.


        1. Hardcoin
          25.08.2017 16:19

          Согласен, это решение лучше, чем текущее, когда у каждого пакета, по сути, есть все права. Но может получиться как с приложениями в Андроиде, когда спустя несколько лет все же добавили возможность приложение поставить, а права не давать.


  1. jehy
    25.08.2017 09:22
    +1

    Да что же все так привязались к бедному npm. Можно подумать, с другими менеджерами пакетов что-то по другому. Вопрос безопасности решается легко — отсматривай, что ты включаешь, и фиксируй версию. Всё. Не надо делать вид, что у вас там чёрный ящик, который надо ограничивать.


    Кстати, автоматические анализаторы пакетов на предмет вредоносного кода уже давно есть, и это тоже нормально, хотя ручной проверки кода ничто не заменит.


    1. NickKolok Автор
      25.08.2017 09:41

      Лично я привязался к npm исключительно потому, что на NodeJS пишу, а на Python и на Ruby — только читаю.


      1. jehy
        25.08.2017 09:55
        +2

        Nuget — всё то же самое
        Composer — всё то же самое
        Maven — то же самое
        В последнее время всё больше компонентов используется напрямую с гитхаба — тем более всё то же самое. И это только примеры с тех языков, с которыми я непосредственно работал.


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


        Более того — даже наличие специальных людей не гарантирует, что в пакете случайно не будет функции, которая разнесёт всю систему (привет яндекс диску, один из релизов которого делал rm /rf).


        И ещё одна бочка дёгтя в вашу каплю мёда — проверка разрешений никоим образом не гарантирует то, что нельзя некорректно воспользоваться выданными разрешениями. Например, пакету может требоваться доступ к файловой системе для записи логов\картинок\кеша\чего угодно. Естественно, этим можно злоупотребить как угодно. Или, к примеру, вы дали права на внешние коннекты. Получите сразу возможность использования зловреда, который отсылает на левый сервер ваши переменные окружения или что угодно ещё.


        Так что такой подход очень вреден — это иллюзия безопасности вместо осознанного принятия рисков и принятия корректных мер по борьбе с ними.


        1. vintage
          25.08.2017 10:31

          Нужен CORS для NodeJS :-)


        1. rraderio
          25.08.2017 10:46

          Например, пакету может требоваться доступ к файловой системе для записи логов\картинок\кеша\чего угодно.

          Но ведь для этого он будет использовать другую библиотеку


        1. NickKolok Автор
          25.08.2017 10:54

          Вы правы, ограничение доступа в ФС и сети по принципу вкл/выкл — это только первый шаг. Не думаю, что так сложно написать доверенный модуль, который бы разрешал доступ к ФС только на чтение/запись и/или в конкретные каталоги. Я не берусь за это по одной простой причине: возможно, это уже сделано, а отягощать базовый paraquire я не хочу. paraquire должен оставаться лёгким и компактным, чтобы его можно было, во-первых, быстро просмотреть, во-вторых, добавляь в репозиторий «намертво», а не через node_modules.

          Переменные окружения — это process.env, о котором я говорил в посте. Разрешение на них даётся отдельно от разрешения на сеть и отдельно от разрешения на ФС.


        1. grossws
          25.08.2017 12:33
          -2

          Maven — то же самое

          Do tell me all about it. С каких пор мавен при притаскивании зависимостей из репозитория выполняет произвольные команды из их pom'ов?


          1. jehy
            25.08.2017 12:46
            +2

            Проблема не только и не столько в командах (настолько "в лоб" хакер вряд ли будет что-то делать), сколько в содержимом зависимостей, которые могут помимо целевого назначения делать всё что угодно. Как пример — недавняя статья по поводу пакета, который переменные оружения отправлял на сервер хакера. Зависимость из мавена может делать это точно так же. И никто по этому поводу почему-то не переживает.


            1. grossws
              25.08.2017 12:49
              +1

              Только в случае java вам надо явно дёрнуть какой-то класс из вредоносной зависимости. В случае проблемы с npm'овскими хуками — вам не нужно дёргать зависимость, всё на pre_install/post_install случится.


              Т. е. для атаки в случае java нужно, чтобы атакуемый сам вызвал вредоносный код (прямо или транзитивно), а в случае npm — достаточно, чтобы вредоносный модуль был в DAGе зависимостей. В общем, поверхность атаки различается значительно.


              1. justboris
                25.08.2017 13:01

                Джавовая библиотека atmosphere, например, отсылает информацию в Google Analytics. В один прекрасный день они могут начать собирать чуть больше информации, а вы и не заметите.


                1. grossws
                  25.08.2017 13:07

                  Перечитайте, что я писал выше. В случае java вы должны прямо или транзитивно дёрнуть нужную библиотеку. В случае npm достаточно того, что она просто в дереве зависимостей. Это две принципиально разные вещи:


                  • потенциально вредоносный код сторонних зависимостей (проблема универсальная, не зависящая от инструментария сборки наблюдается и в java, и в js, и в c++, хоть при использовании централизованных репозиториев, хоть при вендоринге);
                  • наличие хуков, позволяющих злоумышленнику выполнить произвольный код в процессе резолва/установки зависимостей (и это проблема инструментаб, в частности, npm).


                  1. justboris
                    25.08.2017 13:11
                    +2

                    Во-первых, в первом комменте уже написали про -флаг --ignore-scripts, при котором ничего не исполняется. Пропишите этот флаг по дефолту и будет вам счастье.


                    Во-вторых, в этом треде мы обсуждаем вектор атаки, когда вредоносный код активируется в рантайме. Решение автора статьи защищает именно от этого.


                    1. grossws
                      25.08.2017 13:17
                      +1

                      Первый коммент видел, но это решение очень условное, если по умолчанию этот флаг не включен. Кроме того, часть пакетов может поломаться из-за его отключения. Даже если они сделают её включенной по умолчанию и добавят whitelist, то из-за привычки к микропакетам в js-мире поддержка такого whitelist'а будет очень негуманной по ресурсам и большинство забьёт.


                      Ок, если говорить только в контексте активации в рантайме, то я снимаю свои возражения.


                1. grossws
                  25.08.2017 13:20

                  Приведу другой пример в дополнению к вашему: очень популярная библиотека quartz до недавнего релиза при запуске проверяла наличие новых версий и сообщала об этом в логе. Обычно это отключалось через system properties, но в некоторых версиях отключение не работало. Если у вас приложение в закрытом контуре, то такое поведение очень неприятно.


                  В последней версии от этого таки избавились.


              1. jehy
                25.08.2017 13:19
                +1

                А вы часто устанавливаете зависимости, которые потом никак не используете? Если нет, то разницы никакой.


                1. grossws
                  25.08.2017 13:26

                  Я — нет. Я наоборот смотрю пристальным взглядом в mvn dependency:tree -Dverbose=true и проверяю корректность дерева, фиксирую версии принесённые транзитивно, когда надо, выкашиваю лишнее из транзитивки. Но это не самая распространенная практика.


                  При использовании вектора через хуки в фейковую зависимость добавляется реальная и, возможно, реэкспорт методов из реальной библиотеки.


                  При использовании вектора через передачу управления также можно спроксировать всё в реальную библиотеку ниже по дереву зависимостей.


    1. SirEdvin
      25.08.2017 10:17

      Думаю, все потому что, что идея микромодулей в основном используется только в npm. В pypi, например, так не очень принято.


      1. jehy
        25.08.2017 10:20
        +1

        Таки большие модули отличаются только тем, что вы сразу отказываетесь от идеи проверять, что там внутри: )


        Ну и нет — что джава, что PHP приложения сейчас обычно тоже тащат с собой кучу сторонних модулей. Но про это почему-то никто не пишет.


        1. SirEdvin
          25.08.2017 10:26

          Таки большие модули отличаются только тем, что вы сразу отказываетесь от идеи проверять, что там внутри: )

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


          Ну и да, в силу того, что я не очень дружу с документацией, то часто приходится лезть в код и смотреть :)


        1. acmnu
          25.08.2017 11:51

          Тут вопрос скорее в количестве. Редко какой Python проект тянет больше пары десятков зависимостей. Для node.js из-за очень мелкой разбивки средняя на порядок больше. Ситуация с npm ближе к maven, нежели к pip или gems. И в мире джава проблемой сборки уже давно все интересуются, хотя нормального решения вроде не видно.


        1. sshikov
          27.08.2017 21:15
          -2

          Не пишут потому что эта проблема уже давно решена. Ну, не на 100%, вряд ли это вообще возможно, но настолько, насколько это практически важно и можно.


          1. rraderio
            28.08.2017 09:30
            +2

            И как это проблема решена?


            1. sshikov
              28.08.2017 21:37

              Если вы хотите глобального решения — то его не существует. По тривиально простой причине — если у вас библиотека (зависимость) скажем удаляет файл, то на этом уровне нельзя сказать, допустимая это операция, или нет.


              А на уровне менеджера пакетов — какой-нибудь Nexus Pro вполне умеет большинство из того, что тут предлагалось. Просто потому, что история подобного софта намного длиннее чем у NPM. Ну т.е. вы будете иметь всю историю зависимостей проекта, со всеми известными для каждой уязвимостями, вы не сможете (если настроить) подключить к проекту зависимость, про которую заведомо известно, что там дырка, и так далее. Этот модуль Nexus по-моему называется firewall.


              А дальше — уже нужны не только и не столько технические решения.


  1. romenbane
    25.08.2017 09:38
    +1

    Получится ли у вас в последствии переопределить поведение ES modules (import) как require?


    1. NickKolok Автор
      25.08.2017 09:51

      Это ключевое слово, так что вряд ли. Многое зависит от того, будет ли реализация import лежать на v8 (и тогда что-то сделать с ней будет очень проблематично) или же осуществляться силами NodeJS (и тогда есть шанс, что что-то получится). Впрочем, не думаю, что нодовцы отменят require, всё-таки нода не питон, тут так ломать обратную совместимость не принято.


  1. SirEdvin
    25.08.2017 10:18

    Как подобные проблемы решаются в других языках, в частности, в Ruby и Python?

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


  1. FractalizeR
    25.08.2017 12:20
    +1

    Вы не анализировали изменения в быстродействии скриптов с paraquire и без?


  1. bingo347
    25.08.2017 12:52
    +1

    Хорошо было бы добавить возможность мокать встроенные модули ноды, то есть вместо реального fs отдать например Proxy от него, в котором пускаем модуль только к функциям чтения

    Также в текущей реализации вижу проблему внедрения данного решения в существующие проекты, придется править практически всю кодовую базу проекта, патчинг require в одном месте тут выглядит более привлекательным

    Ну и напоследок, допустим Ваш пакет станет очень популярным, Вы можете дать гарантии, что пакет достаточно защищен от атак злоумышленников?


    1. jehy
      25.08.2017 13:21
      +1

      Ну элементарно же! Нужно ограничивать права этого пакета другим пакетом для ограничения прав!


    1. NickKolok Автор
      25.08.2017 17:32

      Гарантий я дать, конечно, не могу, но истинный параноик не будет подключать paraquire через node_modules, а аккуратно прибьёт его гвоздями к проекту. Или хотя бы зафиксирует версию.
      Что же касательно самой защиты песочницы, то тут я тоже не абсолют, но код невелик и открыт для аудита. Простенькие трюки вроде (0,eval)(code) не проходят.

      Да, разграничение доступа к fs — более чем актуально.

      Про патчинг require в одном месте уже идёт обсуждение: github.com/nickkolok/paraquire/issues/1


    1. DjPhoeniX
      26.08.2017 12:48
      +1

      Про разграничение доступа к FS — posix. setreuid, setregid, chroot, и «попробуй выбраться». Для своих сервисов эту комбинацию уже выучил наизусть. И даже бинарные модули ничем не спасают — хоть ты обдёргайся fopen и иже с ними — chroot он «в одну сторону».


      1. NickKolok Автор
        26.08.2017 15:40

        Спасибо, посмотрел. Как я понимаю, эта штука — она для всего процесса? Т.е. порезать в правах только конкретную библиотеку нельзя, не шаманя с многопроцессностью?
        В любом случае, выглядит годно. Можно чуть более подробный мануал по тому, как тремя командами загнать приложуху в chroot jail?


        1. DjPhoeniX
          26.08.2017 21:09

          Для примера — nobody, nogroup, working directory.

          posix.chroot(process.cwd()) // загоняем программу в chroot рабочей папки
          posix.setregid(65534,65534) // задаём группу nogroup
          posix.setreuid(65534,65534) // задаём юзера nobody

          Сначала ставим папку (у непривелигированного юзера может не хватить прав, и команда зафейлится). Потом ставим ID группы (если сначала поставить UID — лишимся прав на смену GID). И под конец UID. Всё.

          Единственная проблема — работает это только из-под рута. Но и просто (из userspace) сделать chroot в папку проекта — тоже хорошая практика. php-fpm только так и загонял, и не раз спасало от шеллов. То-есть, шелл-то в дырявый движок пролезал, но сделать с его помощью в chroot можно было примерно ничего.


  1. sergey-b
    25.08.2017 14:38
    +1

    В Java была технология Java 2 Security Permissions, но после того как всякие полезные библиотеки от Apache стали требовать AllPermissions, то от дальнейшего развития этой фичи отказались.


    1. sshikov
      27.08.2017 21:05

      Что вы такое говорите? Кто отказался, где?


      1. sergey-b
        27.08.2017 21:33

        Как минимум, я в своих проектах давно отказался. А это уже немало.


        1. sshikov
          28.08.2017 21:45

          Ну т.е. надо так понимать, что пруфа не будет?


  1. sshikov
    27.08.2017 21:13

    Мысль-то хорошая, но вот попытка решать данную проблему (безопасность зависимостей) на уровне менеджера зависимостей не кажется удачной. Каждый должен заниматься своим делом, npm — управлять зависимостями. Ну например, Nexus (который в мире java можно считать аналогом npm) вполне себе умеет заниматься такими вещами, как отслеживать, в каких версиях конкретного пакета нашли уязвимости, и сообщать об этом пользователю.


    А решать, что можно разрешить в конкретном приложении, а что нельзя — ну не его это дело. Да и нет у него для этого достаточно информации.