12k
All articles

Модель разрешений Deno: подробный разбор

Права Deno: флаги allow и deny, ограничение доступа, наборы в deno.json, API Permissions и главные риски безопасности.

OpenReplay Team
OpenReplay Team
Модель разрешений Deno: подробный разбор

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

Если вы переходите с Node, ваш первый скрипт на Deno почти наверняка сразу же упадёт с ошибкой разрешений, и решением редко оказывается тот самый универсальный -A, к которому тянется мышечная память. Разобраться, какой флаг добавить и насколько узко его ограничить, — это и есть основная часть кривой обучения.

Это противоположность исторического поведения Node.js по умолчанию, и это самое важное, что нужно понять перед запуском любого скрипта на Deno. В этой статье мы разберём, что заблокировано по умолчанию, все флаги --allow-* и --deny-* вместе с синтаксисом ограничения области действия, дополнения Deno 2.x (приоритет deny, --allow-sys, --allow-import, wildcard для env), наборы разрешений в deno.json, runtime Permissions API и два подвоха с точки зрения безопасности, о которых документация упоминает менее всего.

Ключевые выводы

  • По умолчанию код на Deno не может читать или записывать файлы, открывать сетевые соединения, читать переменные окружения, порождать дочерние процессы, обращаться к системной информации или загружать нативные библиотеки. Каждая возможность подключается отдельно флагом --allow-*.
  • У каждого флага --allow-* есть парный --deny-*, и deny всегда побеждает: --allow-read=. --deny-read=./secrets даёт доступ к каталогу проекта, но оставляет ./secrets недоступным для чтения.
  • В Deno 2 запрещённая операция выбрасывает Deno.errors.NotCapable (переименовано из прежнего PermissionDenied), что отделяет отказы системы разрешений Deno от обычных ошибок ОС.
  • Начиная с Deno 2.5 можно определять именованные наборы разрешений в deno.json и применять их через -P=name (или набор default через просто -P), храня флаги принципа минимальных привилегий в системе контроля версий.
  • Ничто из начального графа статических импортов не проверяется системой разрешений перед загрузкой, а --allow-run запускает дочерние процессы вне песочницы. Это два способа, которыми недоверенный код выходит за её пределы.

Почему Deno безопасен по умолчанию?

Ничто не выполняется с неявными привилегиями: диск, сеть, окружение и порождение процессов остаются закрытыми, пока вы их не откроете. Это архитектурное решение пришло напрямую от Райана Даля, создателя Node, который построил Deno, чтобы развернуть заданную Node модель «полного доступа ко всему». В Deno зависимости не получают собственных неявных полномочий; в Node пакет наследует весь системный ввод-вывод, доступный окружающему процессу, и этот разрыв — самое резкое различие между двумя рантаймами.

Node с тех пор добавил собственную модель разрешений. Она вышла в экспериментальном виде в Node 20 под флагом --experimental-permission, была объявлена стабильной в v23.5.0, а Node 24 отказался от экспериментального написания в пользу обычного --permission. Модель Deno всё же глубже: это поведение по умолчанию, а не опциональный флаг, и она покрывает больше классов возможностей с более тонким ограничением области действия.

Что представляют собой флаги разрешений —allow-* в Deno?

Каждой возможности соответствует один флаг, и большинство флагов принимают аргумент со списком разрешённого. Флаг без аргумента даёт доступ ко всему классу; аргумент его сужает. Просто --allow-net открывает доступ к любому хосту на любом порту, тогда как --allow-net=api.example.com:443 ограничивает программу ровно одним хостом и портом.

ФлагЧто защищаетПример с областью действияПарный deny
--allow-readЧтение файловой системы--allow-read=./data,config.ini--deny-read
--allow-writeЗапись в файловую систему--allow-write=./tmp--deny-write
--allow-netДоступ к сети--allow-net=api.example.com:443--deny-net
--allow-envПеременные окружения--allow-env=PORT,HOST--deny-env
--allow-runДочерние процессы--allow-run=git,deno--deny-run
--allow-sysAPI системной информации--allow-sys=hostname--deny-sys
--allow-ffiНативные библиотеки--allow-ffi=./lib.so--deny-ffi
--allow-importУдалённые импорты по HTTPS--allow-import=jsr.io--deny-import

Обратите внимание: флага --allow-hrtime больше нет. Он был удалён в Deno 2.0, и API высокоточного измерения времени вроде performance.now() теперь доступны всегда.

Когда скрипту нужно разрешение, которое вы не выдали, Deno приостанавливается и запрашивает его интерактивно:

┏ ⚠️ Deno requests net access to "deno.com:443".
┠─ Requested by `fetch()` API.
┗ Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all net permissions) >

Ответьте y, чтобы выдать разрешение однократно, n — чтобы отказать (что вызовет Deno.errors.NotCapable), или A — чтобы разрешить весь класс. В CI передавайте флаги заранее, чтобы ничто не зависало на запросе.

Как разрастался набор флагов: приоритет deny, --allow-sys, --allow-import, wildcard для env

Флаги deny появились в Deno 1.36 (август 2023), и с тех пор у каждого флага --allow-* есть парный --deny-*. Там, где они пересекаются, применяется запрет — это позволяет выдавать широкие права и вырезать из них исключения:

deno run --allow-read=. --deny-read=./secrets app.ts

--allow-sys, существующий ещё с Deno 1.26 (октябрь 2022), контролирует API системной информации, такие как Deno.hostname() и Deno.systemMemoryInfo(). Единственным действительно новым классом возможностей в Deno 2.0 стал --allow-import, который определяет, с каких HTTPS-хостов ваш код может подтягивать модули во время выполнения; обычный HTTP не разрешён никогда, статические импорты автоматически фильтруются по списку, а указание собственных хостов заменяет встроенный набор Deno, а не дополняет его. Используйте --deny-import, чтобы полностью заблокировать конкретные хосты.

Доступ к окружению получил wildcard-суффиксы в Deno 2.1. Вместо перечисления каждой переменной ограничивайте по префиксу:

deno run --allow-env="AWS_*" main.ts

Объявление разрешений в deno.json

Начиная с Deno 2.5 можно определять именованные наборы разрешений в deno.json и применять их через -P=name (или --permission-set=name), храня флаги минимальных привилегий в системе контроля версий вместо перепечатывания их при каждом запуске. Ключи объекта — это имена флагов (read, write, net, env, sys, run, ffi, import), как описано в справочнике по deno.json:

{
  "permissions": {
    "default": {
      "read": ["./deno.json"],
      "env": true,
      "run": { "allow": ["git"] }
    },
    "process-data": {
      "read": ["./data"],
      "write": ["./data"]
    }
  },
  "tasks": {
    "dev": "deno run -P main.ts"
  }
}

Выполните deno run -P=process-data main.ts для именованного набора или deno run -P main.ts для набора default. В Deno 2.5 также добавлена переменная окружения DENO_AUDIT_PERMISSIONS: укажите в ней путь к файлу, и Deno будет добавлять в него JSONL-запись для каждого разрешения, к которому обращается программа, независимо от того, был доступ выдан или отклонён. Это быстрый способ выяснить, что скрипту действительно нужно.

Runtime Permissions API

Запрашивайте состояние разрешений в коде перед ограниченной операцией, чтобы корректно обработать ситуацию вместо падения с ошибкой NotCapable. Deno.permissions предоставляет query, request и revoke, каждый из которых принимает дескриптор вида { name: "net", host: "example.com" }:

const desc = { name: "net", host: "example.com" } as const;

let status = await Deno.permissions.query(desc); // "prompt" | "granted" | "denied"
if (status.state === "prompt") {
  status = await Deno.permissions.request(desc); // triggers the y/n/A prompt
}

if (status.state === "granted") {
  await fetch("https://example.com");
}

await Deno.permissions.revoke(desc); // drop it again

query сообщает текущее состояние без запроса к пользователю, request показывает запрос, если состояние всё ещё prompt, а revoke отзывает возможность. Это позволяет长 долго работающим программам проверять доступ перед обращением к ресурсу и выбирать иной путь, когда доступа нет.

Два подвоха: импорты и --allow-run

Два поведения позволяют недоверенному коду обойти песочницу, и оба стоит хорошо усвоить. Во-первых, всё, что Deno может статически разрешить от вашей точки входа (локальные файлы, пакеты npm и JSR, а также удалённые URL, записанные строковыми литералами), загружается прежде, чем система разрешений получит слово, так что зависимость может прочитать собственный исходный код и обратиться к сети до того, как вообще применится ваш первый флаг --allow-*. Эта поддавка касается только загрузки и ничего больше: в момент выполнения кода каждая операция проверяется снова. --allow-import ограничивает, с каких удалённых хостов можно импортировать, но не делает сами импорты требующими разрешения во время выполнения, поэтому проверяйте сторонний код прежде, чем подключать его.

Во-вторых, --allow-run — самый острый подвох: то, что вы порождаете, становится полноценным процессом и несёт те привилегии, которые ему даёт операционная система, а не тот узкий набор, что вы выдали Deno. Это означает, что --allow-run=deno позволяет скрипту в песочнице перезапустить Deno с --allow-all и полностью выйти из неё. Кроме того, флаг ограничивает лишь то, какой исполняемый файл запускается, но не его аргументы: --allow-run=cat позволяет коду прочитать любой файл через cat. Ограничивайте его конкретными доверенными бинарниками, например --allow-run=git, и учтите, что --allow-ffi несёт риск того же класса, поскольку нативные библиотеки выполняются как машинный код вне проверок уровня JavaScript.

Практическая позиция: выдавайте самый узкий работающий allow-list, добавляйте --deny-* на чувствительные пути и относитесь к --allow-run и --allow-ffi как к границам доверия, а не как к удобствам. Начинайте с нулевых разрешений, запускайте скрипт и добавляйте ровно то, что требуют интерактивные запросы (или лог DENO_AUDIT_PERMISSIONS).

Часто задаваемые вопросы

В чём разница между отказом в интерактивном запросе разрешения Deno и Deno.errors.NotCapable?

Это один и тот же результат, достигнутый разными путями. Когда вы отвечаете «n» на интерактивный запрос или запускаете код без нужного флага, запрещённая операция выбрасывает Deno.errors.NotCapable в Deno 2 (переименовано из прежнего PermissionDenied). Переименование позволяет отличать собственные отказы системы разрешений Deno от обычных ошибок операционной системы, таких как отсутствующий файл, поскольку раньше и то, и другое проявлялось похожим образом.

Разрешает ли --allow-net=example.com также HTTPS на порту 443?

Да. Когда вы указываете хост без порта, например --allow-net=example.com, Deno разрешает подключения к этому хосту на любом порту, включая 443. Чтобы ограничиться одним портом, его нужно указать явно: --allow-net=example.com:443 — тогда все остальные порты этого хоста будут заблокированы. Флаг --allow-net без аргумента открывает любой хост на любом порту.

Можно ли комбинировать --allow-read и --deny-read на пересекающихся путях?

Да, и deny всегда побеждает. Запуск с --allow-read=. --deny-read=./secrets даёт доступ на чтение ко всему каталогу проекта, кроме ./secrets, который остаётся недоступным. Флаги deny имеют приоритет над флагами allow во всех классах возможностей, поэтому этот приём позволяет выдавать широкие права и вырезать из них чувствительные пути вместо перечисления каждого разрешённого файла по отдельности.

Нужен ли --allow-import для использования пакетов npm или JSR?

Нет, для статически импортируемых пакетов не нужен. Всё, что Deno может разрешить от вашей точки входа без выполнения кода, включая пакеты npm и JSR, загружается до обращения к системе разрешений. --allow-import определяет, с каких HTTPS-хостов могут поступать удалённые импорты, а обычный HTTP не разрешён никогда. Со спецификатором, вычисляемым во время выполнения, ситуация иная: динамическому удалённому URL нужен --allow-import, а динамическому локальному пути — --allow-read.

DevTools for the frontend

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.