feat(#243): in-RAM blob sandbox (anonymous GET by UUID, TTL, ETag) + stash_page tool with image mirroring #250
Reference in New Issue
Block a user
Delete Branch "feat/243-blob-sandbox"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Фича #243 — in-RAM blob sandbox для передачи контента агентом
Канал «байты мимо контекста модели И мимо авторизации Docmost»: gitmost держит блоб в RAM, отдаёт анонимным HTTP GET по непрогнозируемому UUID с TTL 1ч. Через контекст агента летит только короткая ссылка. Полностью абстрактно — ни слова про конкретного потребителя (grep
/habr/iпо диффу = 0 совпадений).Компоненты
apps/server/src/integrations/sandbox/sandbox.store.ts—@Injectablesingleton: in-RAMMap<uuid,{buf,mime,sha256,expiresAt}>.put(buf,mime)→{id=randomUUID, sha256, size, expiresAt},get(id)(lazy expiry). Per-blob cap по mime, total-RAM guard с FIFO-эвикцией, unref'd 60s sweep (clear в onModuleDestroy). sha256 при put = ETag.sandbox.controller.ts—GET /api/sb/:id: UUID-regex анти-traversal→404, missing/expired→404, 200 с Content-Type/Length/ETag:"<sha256>"/Cache-Control: private,max-age,immutable, 304 на If-None-Match.sandbox.module.ts—@Global, единый инстанс store для контроллера + tool'ов.stash_page(MCP)/stashPage(in-app), вход{pageId}: сериализует page-json, зеркалит ВСЕ внутренние картинки (fetch по authed loopback → put → переписываетattrs.srcна sandbox-URL), возвращает короткий uri/resource_link. Спека в общийSHARED_TOOL_SPECS.Анонимный роут
JwtAuthGuardнавешан per-controller (нет global APP_GUARD) → отсутствие guard'а делает роут анонимным (как/api/files/public);/api/sbдобавлен вexcludedPathsглобального workspace-preHandler. Капабилити = непрогнозируемый UUID + TTL + TLS, токенов нет.Env
SANDBOX_PUBLIC_URL/SANDBOX_TTL_MS(1ч) /SANDBOX_MAX_BYTES(8MiB) /SANDBOX_MAX_IMAGE_BYTES(20MiB) /SANDBOX_MAX_TOTAL_BYTES(128MiB) — getters + validation + .env.example. uri собирается в wiring-слое, store/пакет env не трогают.Совместимость с потребителем
Сверено point-by-point против контракта
vvzvlad/habr-mcp(склонирован, прочитан): анонимный GET без Authorization ✓; doc как application/json ✓; ETag = quoted lowercase sha256 над отданными байтами, потребитель сверяет sha256 ✓; image discovery поattrs.src(plain string) ✓; image fetch + ETag + Content-Type ✓;resource_linkuri ✓; TTL ~1ч ✓. Без протаскивания специфики потребителя в gitmost.Тесты
mcp 370/370 (вкл. stash-page mock: mirror+rewrite, external CDN не трогается, dedup, failed-image без аборта, not-configured guard); server SandboxStore 9 + SandboxController 6 + ai-chat-tools/env зелёные; server tsc + nest build чисто.
Closes #243
🤖 Generated with Claude Code
Add an ephemeral, process-local blob store so the in-app agent (and the embedded MCP) can hand a large page document and its images to an external consumer WITHOUT routing the bytes through the model context or Docmost auth. - SandboxStore (@Injectable singleton): Map<uuid,{buf,mime,sha256,expiresAt}> in RAM only. put() picks a per-blob cap by mime (image vs doc), enforces a total-bytes RAM guard with oldest-first eviction, and stamps a TTL; get() lazily expires. sha256 computed at put() doubles as the strong ETag. An unref'd sweep interval clears expired entries and is cleared on destroy. - GET /api/sb/:uuid anonymous controller: serves raw bytes with Content-Type, Content-Length and ETag=sha256; 404 on missing/expired/non-UUID (anti- traversal), 304 on a matching If-None-Match. No tokens, no 401 — the capability is the unguessable UUID + short TTL + TLS. Auth-exempt the same way as /api/files/public (no JwtAuthGuard) plus an /api/sb entry in main.ts's workspace-resolution preHandler so a remote consumer with no workspace host is not rejected. - stash_page tool in both layers (MCP resource_link + in-app {uri,size,sha256, images}). client.stashPage serializes the get_page_json shape, mirrors every INTERNAL file/image src (type-agnostic, covers drawio/excalidraw/video/file) into the sandbox under Docmost auth and rewrites src to the sandbox URL; external http(s) srcs are left untouched; dedup by src; a failed image fetch is counted, never aborts the doc. - SANDBOX_PUBLIC_URL / SANDBOX_TTL_MS / SANDBOX_MAX_BYTES / SANDBOX_MAX_IMAGE_BYTES / SANDBOX_MAX_TOTAL_BYTES wired through the environment service + validation + .env.example. - SandboxModule (@Global) provides the shared store to the controller, McpService and AiChatToolsService (same instance for put and get). Tests: SandboxStore (round-trip, sha256, TTL lazy + sweep, caps, eviction), SandboxController (200+ETag+CT+CL, 404 missing/expired/non-UUID, 304), and a mock-HTTP stashPage test (mirror+rewrite internal, keep external, dedup, failed image counted, returns only a link). Interoperates with the vvzvlad/habr-mcp consumer's anonymous-GET + sha256-ETag + resource_link contract. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>Правки по ревью — готово ✅ (commit
6eb335d5)Все замечания закрыты в
feat/243-blob-sandbox. Сборкаpackages/mcp/build/пересобранаtscи сверена сsrc. Тесты зелёные: MCP 344 unit + 34 mock, сервер 34 (sandbox + environment).🔴 Must fix
fetchInternalFile— добавлена чистаяresolveInternalFilePath(src)(packages/mcp/src/lib/internal-file-urls.ts): отвергает%2e/%2f, канонизирует путь черезnew URL(src, "http://internal.invalid").pathnameи требуетstartsWith("/api/files/")—..схлопывается и выход из/api/files/отклоняется доclient.get.fetchInternalFileтеперь фетчит только возвращённый относительный путь; вводящий в заблуждение комментарий переписан. Отклонённый src идёт вfailed, документ не рушится.stash_pageвREADME.mdиREADME.ru.md.expiresAt— убрано изSandboxPutResultи изreturnвput();SandboxEntry.expiresAt(нужно контроллеру/get/sweep) не тронут.⚠️ Стабильность / suggestions
stashPageфиксирует per-node оригинальныйsrc, и если блоб вытеснен FIFO более поздним put в этой же стэш-операции, откатываетsrcузлов к оригиналу и пересчитываетmirrored/failed(через новые пробыhas/evictу sink). Битых ссылок в документе и вранья в счётчике больше нет.SANDBOX_TTL_MS ≤ 0/ нечисловой —getSandboxTtlMsотвергает невалидное значение, откатывается к дефолту 3600000 мс и предупреждает один раз (без спама на каждый put).console.warnбез гейта наDEBUG(без тел блобов); под него же попадают и отклонённые traversal-срцы.isInternalFileUrl— снятexport(используется только внутри файла).🏛️ Архитектура (обе опции реализованы)
SandboxStore.putAndLink(buf, mime)владеет формой URL; маршрут вынесен вSANDBOX_ROUTE_SEGMENT/SANDBOX_API_PATH(sandbox.constants.ts) и используется контроллером (@Controller),main.ts(excludedPaths) иputAndLink. Дублирующие замыкания и ставшая ненужной инъекцияEnvironmentServiceубраны изMcpServiceиAiChatToolsService.stash_page(tool-specs.ts) и в секцииSANDBOX_PUBLIC_URLв.env.example: блобы привязаны к инстансу, при multi-replica без sticky-сессий консьюмер может попасть на другой инстанс и получить 404.🧪 Тесты
packages/mcp/test/unit/internal-file-urls.test.mjs: приём нормального src и отклонение traversal/%-вариантов; ветка/files/ → /api/files/; рекурсияcollectInternalFileNodesво вложенныйcontent.stash-page.test.mjs: self-eviction-revert, throw на put документа освобождает блобы, пустой ответ →failed, отсутствиеContent-Type→application/octet-stream.sandbox.controller.spec.ts:If-None-Matchwildcard*/ weakW// список через запятую; ассертCache-Controlиmax-age/ttlSeconds.environment.service.spec.ts: валидацияSANDBOX_TTL_MS(0/-5/abc→ дефолт, валидное положительное → проходит).Security (must-fix): - sandbox.controller: the anonymous GET /api/sb/:id response now sets X-Content-Type-Options: nosniff, a restrictive CSP, and Content-Disposition= attachment for any mime outside a raster-image allowlist (png/jpeg/gif/webp/ avif). entry.mime is attacker-controlled, so an evil.svg/evil.html could otherwise execute script inline on the Docmost origin (stored XSS). Mirrors the public attachment route's hardening. Stability: - client.stashPage: reconcile mirrors AFTER the final document put, not only before it. The doc blob is the newest entry and FIFO eviction drops the oldest = this stash's own images, so the stored doc could reference an evicted blob (consumer 404) and over-report images.mirrored. A bounded loop now reverts doc-put-evicted mirrors, drops the stale doc blob, and re-puts until stable. Regenerated packages/mcp/build/. - sandbox.controller: emit Cache-Control on the 304 branch too (ttlSeconds is computed before the conditional check). Docs: - Bump the MCP tool count 39 -> 40 across all READMEs and AGENTS.md (the registry now exposes exactly 40 tools). Refactor: - SandboxStore.asSink() centralizes the {put,has,evict} sink + uri<->id mapping; the embedded-MCP and in-app agent-tools wiring sites share it. Tests: - security headers (inline vs attachment, nosniff, CSP), 304 Cache-Control, putAndLink URL form, has()/remove(), asSink() round-trip, getSandboxPublicUrl (trailing-slash trim + APP_URL fallback), and a stash test where the doc put itself evicts a mirrored image. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>Code review — in-RAM blob sandbox +
stash_pageВердикт: Request changes — изменение по сути отличного качества (security, stability, регрессии и покрытие тестами — чисто), но есть один обязательный к правке пункт перед merge: внесённый этим PR «мёртвый» импорт + устаревший комментарий в
mcp.module.ts. Это change-introduced debt с малым рантайм-радиусом, но по правилам — must-fix, а не отложенный «nice-to-have». Поправить — и можно мёржить.Отревьюено:
git diff --merge-base develop feat/243-blob-sandbox(33 файла, +2222/−19). Сгенерированныеpackages/mcp/build/*.jsпроверены только на синхронность сsrc/*.ts— синхронны. Прогон вёлся 8 специализированными ревьюерами (security, stability, conventions, documentation, regressions, test-coverage, simplification, architecture).Must fix before merge
EnvironmentModuleи комментарий вMcpModule—apps/server/src/integrations/mcp/mcp.module.ts:5-15Этот PR удалил
EnvironmentServiceизmcp.service.ts(импорт + параметр конструктора, заменены наSandboxStore). После этого ни один провайдерMcpModule(McpService,McpController,mcp-auth.helpers) больше не потребляетEnvironmentService— проверено на head ветки. ИмпортEnvironmentModuleстал мёртвым (формально безвреден, т.к. модуль@Global), а комментарий по-прежнему перечисляет его как поставщикаEnvironmentServiceдля этого модуля — обоснование импорта теперь ложное.Fix: удалить
EnvironmentModuleизimports(он@Global, прочие зависимости резолвятся и так) и убрать соответствующую фразу из комментария; либо, если импорт оставляют для наглядности, переписать комментарий, чтобы он не приписывал импорт несуществующей зависимостиMcpService.Non-blocking findings
[suggestion][stability] Валидировать неположительные
SANDBOX_MAX_BYTES/MAX_IMAGE_BYTES/MAX_TOTAL_BYTES—apps/server/src/integrations/environment/environment.service.ts:384-407getSandboxTtlMs()отбрасывает<= 0/нецелые и откатывается к дефолту, а три новых байтовых геттера делают голыйparseIntбез пост-валидации;@IsNumberStringпропускает"0","-5","1.5". ПриSANDBOX_MAX_TOTAL_BYTES=0/отриц. каждыйstash_pageпадает с невнятной ошибкой «exceeds the total store cap of 0 bytes»,"...5"молча округляется. Бесконечного цикла эвикции нет (проверкаbuf.length > maxTotalсрабатывает доwhile). Fix: по аналогии сgetSandboxTtlMs()— при!Number.isInteger(parsed) || parsed <= 0варнинг + дефолт.[suggestion][simplification] Заменить самодельный
UUID_REна существующий валидаторuuid—apps/server/src/integrations/sandbox/sandbox.controller.ts:6-7В проекте уже есть зависимость
uuidи устоявшийся паттернimport { validate as isValidUUID } from 'uuid'(attachment.controller.ts— 4 использования), на который ссылается и сам докстринг контроллера. ОтдаютсяrandomUUID()(v4), которыеuuid.validate()принимает. Заодно убираются две копии regex в тестах.[suggestion][simplification] Заинлайнить однострочную обёртку
buildSandboxConfig()—apps/server/src/integrations/mcp/mcp.service.ts:119-125Тело —
return this.sandboxStore.asSink();, единственный вызов. Лишний слой косвенности без собственной логики; пояснительный комментарий уже продублирован в месте wiring вai-chat-tools.service.ts.[suggestion][simplification] Сделать
has()/remove()приватными —apps/server/src/integrations/sandbox/sandbox.store.tsИх единственный не-тестовый вызыватель —
asSink()того же класса;remove()— однострочная обёртка над приватнымevict(). Опционально (покрыты юнит-тестами напрямую).[suggestion][documentation] Добавить запись в
[Unreleased] > AddedCHANGELOG —CHANGELOG.md:11-13Раздел
[Unreleased] > Addedфиксирует свежие пользовательские фичи по номеру issue (#198, #222, #228), а #243 (новый MCP-инструментstash_page, анонимныйGET /api/sb/:id, пятьSANDBOX_*env) диффом не затронут. Non-blocking: AGENTS.md формально описывает датированный блок как сборку на релизе, но де-факто Unreleased-записи в фиче-PR добавляют.[suggestion][documentation] Упомянуть анонимный маршрут
/api/sbтам, где AGENTS.md описывает auth-модель —AGENTS.md:244Появился второй анонимный, освобождённый от workspace-gate маршрут (рядом с
/api/files/public) — новое исключение к «most /api routes». Прямого противоречия нет (раздел обзорный), confidence низкий — на усмотрение автора.Test coverage
Покрытие необычно полное — LGTM. Осмысленно заасерчены:
sandbox.store(TTL/lazy-expiry, per-blob mime-cap, total-RAM guard + FIFO-эвикция, 60s sweep,onModuleDestroy, sha256/ETag),sandbox.controller(invalid-UUID/missing/expired→404, happy-path с заголовками, 304 на все вариантыIf-None-Match: quoted/bare/*/W//comma-list),stash_page(mirror+rewrite, внешний CDN не трогается, dedup, упавшая картинка не прерывает операцию, not-configured guard), env-геттеры с реальной логикой (success + invalid). Непокрыто только тривиальное: три cap-геттера = голыйparseIntpass-through и декларативные валидаторы (как и весь существующий аналогичный код проекта) — не пробел.Architecture & design (forward-looking, non-blocking)
Обе развилки — осознанные, текущая реализация когерентна и мёржу не мешает; решает автор.
1. In-RAM-only хранилище vs существующая абстракция
StorageDriver(integrations/sandbox/sandbox.store.tsvsintegrations/storage/)Введён второй, процесс-локальный RAM-only механизм хранения блобов, отдельный от durable-абстракции (local/S3/Azure). Следствие честно задокументировано (
.env.example, описание tool-spec): за балансировщиком на несколько реплик без sticky-сессийputна ноде A иGETна ноде B вернёт 404, неотличимый от истечения TTL. Публичный контракт (GET /api/sb/:id) — трудноменяемая часть; бэкенд за ним менять легко, отсрочка дёшева.StorageDriver(ephemeral-префикс + TTL-cleanup) (effort: m). + multi-replica-safe для S3/Azure из коробки, переиспользует единственный storage-seam, переживает рестарт в пределах TTL. − блобы на диск/в object-store (нужен надёжный TTL-delete, риск orphan'ов), теряется «стирание на рестарте», local-драйвер всё равно не шарится.2. Дублирование
internal-file-urls.tsс хелперамиeditor-ext(packages/mcp/src/lib/internal-file-urls.ts)Заново реализованы
isInternalFileUrl/normalizeFileUrl, уже существующие вpackages/editor-ext(сам заголовочный комментарий это признаёт — дубль, чтобы ESM-пакет mcp не зависел от сборки editor-ext). Действительно новая логика (resolveInternalFilePath— SSRF/traversal-guard, иcollectInternalFileNodes) — легитимно пакето-специфична. Риск расхождения низкий (два тривиальных предиката, стабильные константы), но это введённый дубль.Что специально проверено и признано корректным
nosniff+ ограничительный CSP +Content-Disposition: attachmentдля не-растровых mime (svg/html/json), inline только для allowlist растров → нет stored-XSS даже при влиянии на mime; missing и expired оба → 404 (нет утечки); SSRF исключён —attrs.srcсобирается только при префиксе/api/files/или/files/,resolveInternalFilePathблокирует%2e/%2f/..///hostи удерживает путь под/api/files/; секретов в диффе нет; RAM ограничен (caps до вставки + FIFO + sweep).put/get/sweep/evictсинхронны (нет await между read и mutate) → нет внутри-store-гонок; байтовый учёт консистентен; блоб не вытесняет сам себя; sweep-таймерunref'нут и очищается вonModuleDestroy; одна упавшая картинка не прерывает операцию (конкурентность ≤5, таймаут 30с, потолок 64 MiB на fetch);for(;;)-реконсиляция FIFO завершается; при ошибке put блобы операции откатываются./api/sbвexcludedPathsне коллизирует с существующими маршрутами;@Global SandboxModule— порядок импорта неважен, дублейAPP_GUARDнет; изменения shared MCP-файлов аддитивны; build-артефакты пересобраны и побайтово совпадают.Bugs: critical/warning-дефектов корректности или безопасности — ноль. Единственный must-fix — косметический change-introduced debt (мёртвый импорт + устаревший комментарий) в
mcp.module.ts. Остальное — необязательные улучшения и две архитектурные развилки на усмотрение автора.🤖 Сгенерировано code-review-orchestrator (8 параллельных ревьюеров + judge-pass).
Must-fix: - mcp.module: drop the now-dead EnvironmentModule import (and its stale comment). McpService no longer injects EnvironmentService; EnvironmentModule is @Global and imported at the app root, so DI still resolves. Stability: - environment.service: route getSandboxTtlMs + the three SANDBOX_MAX_*_BYTES caps through a shared getPositiveIntEnv() helper that warns once per key and falls back to the default on a non-integer or <= 0 value (previously the byte caps did a bare parseInt, so SANDBOX_MAX_TOTAL_BYTES=0 made every stash_page fail against a 0-byte cap). TTL behavior is unchanged. Simplification: - sandbox.controller: replace the homemade UUID_RE with the project's shared `uuid` validator (import { validate as isValidUUID } from 'uuid'), matching the attachment routes; update the spec fixtures to valid v4 UUIDs. - mcp.service: inline the single-caller one-liner buildSandboxConfig() to this.sandboxStore.asSink() at the wiring site. Docs: - CHANGELOG: add an [Unreleased] > Added entry for #243 (stash_page tool, anonymous GET /api/sb/:id, five SANDBOX_* env vars). - AGENTS.md: note that GET /api/sb/:id is in the workspace-gate preHandler's excludedPaths and is fully tokenless, unlike /api/files/public/... which still resolves a workspace and needs an attachment JWT. Tests: cap-getter validation (0/-5/abc -> default, valid -> parsed), updated UUID fixtures. apps/server jest sandbox/environment/mcp: 233 pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>Код-ревью — in-RAM blob sandbox (анонимный GET по UUID, TTL, ETag) +
stash_pageс зеркалированием картинокВердикт: Approve with comments (одобрить с замечаниями).
Изменение спроектировано аккуратно и хорошо покрыто тестами. Многоаспектное ревью (security, stability, conventions, documentation, regressions, test-coverage, simplification, architecture); 4 аспекта — чистый LGTM (security, stability, regressions, conventions). Блокирующих замечаний нет — ни одно из найденных не меняет вердикт мерджа. Ревью охватывало diff относительно merge-base с
develop(35 файлов, +2280/−26); сгенерированныеpackages/mcp/build/*.jsисключены (правятся изsrc/).Проверено пристально и оказалось корректным:
GET /api/sb/:id: глобальногоAPP_GUARDв приложении нет, поэтому отсутствиеJwtAuthGuardдействительно делает роут анонимным — ровно какGET /api/files/public/.... Отдаются только блобы изMapпо валидированному UUID, traversal-поверхности нет, токены не утекают, угла для enumeration нет.resolveInternalFilePath: защита держит литеральный..,%2e/%2f, backslash, таб/перевод строки, null-байт, protocol-relative и absolute-external (хост отбрасывается, проходят только pathname с префиксом/api/files/). Запрос остаётся на loopback, CASL работает за счёт пользовательского JWT.X-Content-Type-Options: nosniff+ allowlist inline-mime, всё не из списка (svg/html/json) форсится вattachment; mime санируется.stashPage:totalBytesне дрейфует, циклы эвикции терминируются, счётчикиmirrored/failedне уходят в минус, блобы освобождаются при ошибке put дока. DI после удаленияEnvironmentModuleизmcp.module.tsкорректен (EnvironmentServiceтам больше не используется).Обязательно до мерджа
[warning][test-coverage] Добавить тесты
resolveInternalFilePathна accept-путь absolute-URL/scheme и backslash —packages/mcp/src/lib/internal-file-urls.ts:61-87. Тесты покрывают только..и%2e/%2f-reject. Не зафиксирован неочевид��ый accept-путь:http://evil.com/api/files/x/y.pngотбрасывает хост и возвращает/files/x/y.png(это безопасно — baseURL axios на loopback, хост игнорируется, реального SSRF нет), но именно отбрасывание хоста ничем не закреплено. Так как это единственная защита от SSRF/traversal для content-controlledsrc, будущий рефакторинг (например, переход на prefix-only проверку) может молча открыть обход без падающего теста. Fix: добавить кейсы — absolute-URL с чужим хостом резолвится в/files/x/y.png;/api/files\..\auth\whoamiбросает;https://evil.com/api/auth/whoamiбросает.[suggestion][documentation] Согласовать MCP-результат
stash_page(толькоresource_link) с задокументированной формой{ uri, size, sha256, images }—packages/mcp/src/index.ts:414-433. Описание вtool-specs.tsи оба README обещаютReturns { uri, size, sha256, images:{mirrored, failed} }. Это верно для in-app пути (ai-chat-tools.service.tsотдаёт полный объект), но MCP-транспорт возвращает толькоresource_linkсuri/name/mimeType/size— безsha256иimages. Радиус мал (sha256 доступен как ETag блоба, ссылка — основной payload), поэтому non-blocking. Fix: либо добавитьstructuredContent: { uri, sha256, size, images }рядом сresource_linkвindex.ts, либо смягчить общее описание (по MCP возвращаетсяresource_link, а sha256 — это ETag блоба).[suggestion][test-coverage] Покрыть catch-ветку
new URL—packages/mcp/src/lib/internal-file-urls.ts:72-78. Ветка «Invalid internal file src» достижима (например,http://[), но не тестируется. Fix: добавитьassert.throwsна malformed-src.[suggestion][test-coverage] Проверить one-shot-warn дедуп в
getPositiveIntEnv—environment.service.ts:360-375. Тесты проверяют fallback-значение, но не то, чтоlogger.warnсрабатывает один раз на ключ (ради чего и заведёнinvalidPositiveIntWarned). Fix: кейс с invalid env, спай наlogger.warn, два вызова геттера, ожидание ровно одного warn.Покрытие тестами
Покрытие сильное. Полноценно протестированы:
SandboxStore(per-blob/total caps, FIFO-эвикция, lazy expiry, sweep,has/remove,putAndLink/asSinkround-trip, учёт байт),SandboxController(200 с ETag/Cache-Control/nosniff/CSP, inline-vs-attachment по mime, 404 на non-UUID/missing, 304 для quoted/bare/W//списка/*),stashPage(зеркалирование+rewrite, dedup, внешний CDN не трогается, failed-image без аборта, not-configured guard, и все три ветки FIFO-реконсиляции, включая doc-put-throws), геттеры env (fallback). Незакрытые места — только три перечисленных выше; главное из них — security-critical accept-путьresolveInternalFilePath.