Коротка відповідь. Permission Model – це стабільний (з v23.5) механізм Node.js, який за прапорцем --permission забороняє процесу все, що ти явно не дозволив: читання/запис файлів, мережу, child processes, worker-и, нативні аддони. Це захист від «залежність робить більше, ніж обіцяла», а не повноцінна пісочниця: символічні лінки, вже відкриті дескриптори і код у межах дозволених шляхів модель не контролює. Використовуй її як дешевий додатковий шар для CLI, білд-скриптів і AI-агентів – поверх, а не замість контейнерів і рев'ю залежностей.
Threat model: від чого це захищає
Чесно сформулюємо проблему. Коли ти запускаєш npm install && npm run build, на твоїй машині виконується код сотень пакетів – з повними правами твого користувача. Прочитати ~/.ssh/id_ed25519, ~/.aws/credentials, надіслати їх на чужий сервер – для скомпрометованої залежності це три рядки коду. Про те, як typosquatting-пакети потрапляють у проєкти через AI-генерований код, я вже писав у чеклісті перевірки AI-коду – Permission Model закриває наступний рубіж: навіть якщо поганий пакет уже в node_modules, його можливості обмежені.
Той самий аргумент працює для AI-агентів у терміналі: процес, який запускає згенерований код, хочеться тримати на короткому повідку.
Node.js у документації прямо називає модель «ременем безпеки»: вона не захищає від зловмисного коду, якому ти сам дав права, – вона гарантує, що процес не вийде за явно окреслені межі.
Як це вмикається
Два режими:
--permission– enforce: усе, що не дозволено, падає зERR_ACCESS_DENIED;--permission-audit– audit: нічого не блокується, але кожне «порушення» публікується в diagnostics channel. Ідеально, щоб спочатку дізнатися, куди насправді лізе твій процес.
Капсули дозволів – окремі прапорці:
--allow-fs-read=<шлях> # читання: шляхи, wildcard * або повний доступ
--allow-fs-write=<шлях> # запис
--allow-child-process # spawn/exec
--allow-worker # worker_threads
--allow-net # мережа
--allow-addons # нативні аддони
--allow-wasi # WASIШляхи можна передавати відносні, абсолютні, кілька разів і з wildcard (/home/test*). Файл-ентрипоінт автоматично додається до списку читання – інакше Node не зміг би запустити сам скрипт.
Практика: CLI з мінімальними правами
Задача з брифу нашого ж блогу: скрипт читає Markdown із ./content, пише результат у ./dist – і не має жодних причин чіпати ~/.ssh чи ходити в мережу.
node --permission \
--allow-fs-read=./content/ \
--allow-fs-write=./dist/ \
build-content.mjsЩо станеться, якщо залежність усередині build-content.mjs спробує зазирнути в домашню директорію:
Error: Access to this API has been restricted
code: 'ERR_ACCESS_DENIED',
permission: 'FileSystemRead',
resource: '/Users/you/.ssh/id_ed25519'Процес навіть не дізнається, чи існує файл. Мережі немає взагалі – --allow-net не передано, тож ексфільтрувати дані нікуди. child_process теж мертвий: обхід через curl у дочірньому процесі не спрацює.
Перед enforce-режимом корисно тиждень пожити в аудиті:
node --permission-audit build-content.mjsimport diagnostics_channel from 'node:diagnostics_channel';
diagnostics_channel
.channel('node:permission-model:fs')
.subscribe((msg) => {
console.warn(`[audit] ${msg.permission}: ${msg.resource}`);
});Так ти отримаєш реальний список ресурсів, які потрібні процесу, – і не зламаєш продакшен першого ж дня суворими правилами.
Є і runtime-API: process.permission.has('fs.read', path) для перевірки та process.permission.drop(...) – незворотне звуження прав «на льоту» (прочитав конфіг на старті – відмовився від права читати його директорію назавжди).
Що модель НЕ гарантує
Це найважливіший розділ – документація Node.js тут приємно чесна:
- Символічні лінки не зупиняють. Symlink усередині дозволеної директорії, що вказує на
~/.ssh, – документований обхід. Якщо обробляєш чужі файли, перевіряй лінки сам. - Уже відкриті file descriptors живуть поза моделлю (операції типу
fchmodна відкритих fd під моделлю просто вимкнені повністю). - Worker-и не успадковують обмеження – тому
--allow-workerі варто давати лише свідомо. - Деякі прапорці (
--env-file, конфіги OpenSSL) читають файли до ініціалізації моделі. - Код у межах дозволених шляхів може робити будь-що: дозволив писати в
./dist– залежність може писати в./distсміття. - Окремі підсистеми мають власні дірки: наприклад,
node:sqliteрозширення не завантажуються, а деякі шляхи доступу модель не покриває.
Звідси правильна ієрархія захисту: рев'ю залежностей → Permission Model → контейнер/VM з обмеженнями ОС. У CI це поєднується природно: контейнер дає зовнішній периметр, --permission – внутрішній, дрібнозернистий.
Типові помилки
- Увімкнути enforce одразу в проді. Спочатку
--permission-auditі тиждень логів – інакше перший же legitimate edge case покладе сервіс. --allow-fs-read=*«щоб працювало». Це вимикає половину сенсу. Витрать 10 хвилин на реальний список шляхів з аудиту.- Вважати це пісочницею для недовіреного коду. Запускати чужий довільний код треба в ізоляції рівня ОС/VM – модель лише зменшує радіус ураження.
- Забути про тести. Тест-ранер теж треба запускати з тими ж прапорцями, інакше в CI усе зелене, а в проді –
ERR_ACCESS_DENIED. - Дозволити child_process «на всяк випадок». Дочірній процес успадковує повні права користувача – це найширша дірка з усіх прапорців.
Практичне завдання
Візьми будь-який свій скрипт із package.json (build, кодоген, лінтер контенту) і:
- Запусти його з
--permission-audit+ підписка на diagnostics channel – зафіксуй, куди він реально ходить. - Склади мінімальний набір
--allow-*і переведи скрипт в enforce-режим. - Додай у скрипт навмисну спробу прочитати
~/.ssh/known_hostsі переконайся, що отримуєшERR_ACCESS_DENIEDз правильнимresource. - Бонус: винеси прапорці в
node.config.json(--experimental-default-config-file), щоб команда не розповзалась по Makefile-ах.
Пів години роботи – і твій білд більше не має прав, яких йому ніхто не обіцяв.