# Node.js Permission Model: як обмежити fs, child_process і мережу для процесу

> Permission Model у Node.js: прапорці --permission і --allow-*, режим аудиту, чесні межі захисту і практичний приклад CLI, якому дозволено ./content і ./dist, але не ~/.ssh.

- Автор: Юра Скиба (https://cookiesoftware.io)
- Опубліковано: 2026-10-01
- Категорія: Backend і System Design
- Canonical: https://cookiesoftware.io/blog/nodejs-permission-model

---

**Коротка відповідь.** 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-коду](/blog/ai-kod-chek-list-perevirky) – Permission Model закриває наступний рубіж: навіть якщо поганий пакет уже в `node_modules`, його можливості обмежені.

Той самий аргумент працює для [AI-агентів у терміналі](/blog/claude-code-dlia-react): процес, який запускає згенерований код, хочеться тримати на короткому повідку.

Node.js у документації прямо називає модель «ременем безпеки»: вона **не** захищає від зловмисного коду, якому ти сам дав права, – вона гарантує, що процес не вийде за явно окреслені межі.

## Як це вмикається

Два режими:

- `--permission` – **enforce**: усе, що не дозволено, падає з `ERR_ACCESS_DENIED`;
- `--permission-audit` – **audit**: нічого не блокується, але кожне «порушення» публікується в diagnostics channel. Ідеально, щоб спочатку дізнатися, куди насправді лізе твій процес.

Капсули дозволів – окремі прапорці:

```bash
--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` чи ходити в мережу.

```bash
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-режимом корисно тиждень пожити в аудиті:

```bash
node --permission-audit build-content.mjs
```

```js
import 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` – внутрішній, дрібнозернистий.

## Типові помилки

1. **Увімкнути enforce одразу в проді.** Спочатку `--permission-audit` і тиждень логів – інакше перший же legitimate edge case покладе сервіс.
2. **`--allow-fs-read=*` «щоб працювало».** Це вимикає половину сенсу. Витрать 10 хвилин на реальний список шляхів з аудиту.
3. **Вважати це пісочницею для недовіреного коду.** Запускати чужий довільний код треба в ізоляції рівня ОС/VM – модель лише зменшує радіус ураження.
4. **Забути про тести.** Тест-ранер теж треба запускати з тими ж прапорцями, інакше в CI усе зелене, а в проді – `ERR_ACCESS_DENIED`.
5. **Дозволити child_process «на всяк випадок».** Дочірній процес успадковує повні права користувача – це найширша дірка з усіх прапорців.

## Практичне завдання

Візьми будь-який свій скрипт із `package.json` (build, кодоген, лінтер контенту) і:

1. Запусти його з `--permission-audit` + підписка на diagnostics channel – зафіксуй, куди він реально ходить.
2. Склади мінімальний набір `--allow-*` і переведи скрипт в enforce-режим.
3. Додай у скрипт навмисну спробу прочитати `~/.ssh/known_hosts` і переконайся, що отримуєш `ERR_ACCESS_DENIED` з правильним `resource`.
4. Бонус: винеси прапорці в `node.config.json` (`--experimental-default-config-file`), щоб команда не розповзалась по Makefile-ах.

Пів години роботи – і твій білд більше не має прав, яких йому ніхто не обіцяв.
