Две проблемы в том, как транзактор принимает транзакции (tx) от клиента. Найдены на ревью, были в develop, к конкретной фиче не относятся. Обе исправлены и покрыты unit-тестами.
| # | Проблема | Чем грозило | Где исправлено |
|---|---|---|---|
| 1 | Пути полей в tx могли вести в прототип объектов | Порча общего состояния процесса транзактора, вплоть до ослабления проверок доступа у всех его сессий | objvalue.ts, normalizeTx.ts |
| 2 | Признак «tx от триггера» брался из поля, которое задаёт клиент | Запись в пространства без членства, обход ограничений гостей и режима только для чтения | spaceSecurity.ts, guestPermissions.ts, seatLimits.ts |
Ключи operations у TxUpdateDoc и attributes у TxMixin - это пути полей через точку, их применяет setObjectValue (foundations/core/packages/core/src/objvalue.ts). Служебные сегменты __proto__, constructor, prototype не отсекались, поэтому запись по такому пути попадала не в документ, а в прототип, общий для всех объектов процесса. Это глобальное состояние транзактора: оно действует на все сессии и воркспейсы процесса до его перезапуска, в том числе на проверки доступа. Та же tx рассылается клиентам и применяется в их LiveQuery.
NormalizeTxMiddleware проверял только типы полей tx, ключи внутри operations и attributes никто не проверял.
isUnsafeKey(objvalue.ts) - путь, в котором есть сегмент__proto__,constructorилиprototype. Сравнение по целому сегменту:constructorNameилиmy__proto__безопасны.setObjectValueтакой путь пропускает без записи. Это защита на всех путях применения tx, включая клиентскийLiveQuery.NormalizeTxMiddleware.sanitizePathsна входе удаляет такие ключи изattributes(create, mixin),operations(update), из ключей внутри операторов, из целей$renameи из tx внутриTxApplyIf. Остальная часть tx применяется, в лог уходит предупреждениеunsafe field path removed from txс id tx, классом и аккаунтом.
Почему удаление, а не ошибка: в модели нет атрибутов с такими именами, легитимный клиент их не шлёт. Ошибка в setObjectValue ломала бы обработку потока tx у клиентов, а молчаливый пропуск - нет.
foundations/core/packages/core/src/__tests__/objvalue.test.ts, блокpaths reaching Object.prototype:setObjectValueиTxProcessor(update, mixin) применяют обычные поля и не трогают прототип;isUnsafeKeyсравнивает целые сегменты.foundations/server/packages/middleware/src/tests/normalizeTx.test.ts, блокfield paths reaching Object.prototype: удаление в update, операторах, mixin, create иTxApplyIf; обычные пути не меняются, предупреждения нет.
Tx, которые порождают триггеры, проходят без проверок доступа: триггер действует от имени системы и пишет туда, куда сам пользователь не может. Распознавали их по tx.space === core.space.DerivedTx. Но space - поле самой tx, его задаёт клиент, а NormalizeTxMiddleware проверяет только, что это строка. Поэтому пропуск срабатывал и для клиентских tx:
SpaceSecurityMiddleware.checkWriteAccess- проверка записи в пространство (членство в приватном);GuestPermissionsMiddleware- правила для гостей;SeatLimitsMiddleware- режим только для чтения для участника без места.
Для объектов класса Space это уже было исправлено раньше (FUSIO-1316): там решение принималось по флагу derived. Для остальных документов осталась проверка по tx.space, а тест allows a trigger-derived tx regardless of membership закреплял такое поведение для клиентского пути.
- Серверные derived-tx (триггеры,
ChunterMiddleware,NotificationMiddleware) входят в пайплайн черезcontext.derived- следующий послеMarkDerivedEntryMiddleware(server/server-pipeline/src/pipeline.ts), то есть ниже трёх мидлварей выше. В ихtx()они не попадают. - В
SpaceSecurityMiddlewareони приходят черезhandleBroadcastсderived = true. - На время их обработки сервер ставит
contextData.isTriggerCtx = true(enterTriggerCtxвtriggers.ts,NotificationMiddleware).SessionDataсоздаётся заново на каждый запрос (ClientSession.includeSessionContext), клиент этот флаг выставить не может. - Внешние сервисы, которые сами шлют
DerivedTx(github, notifications, activity), подключаются подsystemAccountUuidи проходят проверки как системный аккаунт. - Из клиента
DerivedTxшлёт толькоEditTagElement.svelte: при переименовании тега обновляет найденныеTagReference. Такой запрос безspaceвозвращает ссылки только из пространств, где пользователь участник, и из системных (addSecurity), а писать тудаcanWriteSpaceразрешает, поэтому проверка записи этот сценарий не ломает. Гость и участник без места до ссылок не доходят: им запрещено уже обновление самого тега.
Пропуск триггеров сохранён, но признак теперь серверный, tx.space не учитывается:
SpaceSecurityMiddleware.checkWriteAccessпропускает tx приderived(пришла черезhandleBroadcast) илиisTriggerCtx === true;GuestPermissionsMiddlewareиSeatLimitsMiddlewareпропускают tx приisTriggerCtx === true.
Клиентская tx с DerivedTx не отклоняется, а проверяется как обычная.
Оставлено как есть сознательно: tx с DerivedTx не пишется в журнал DOMAIN_TX (txPush.ts) и пропускается activity и вебхуками. Проверки доступа она теперь проходит как обычная, поэтому записать ею можно только то, что пользователь и так вправе записать.
foundations/server/packages/middleware/src/tests/spaceSecurity.test.ts, блокwrite access into non-space objects: tx триггера проходит черезhandleBroadcast; клиентские create, update и remove сDerivedTxв чужое приватное пространство отклоняются; в контексте триггера tx проходит.foundations/server/packages/middleware/src/tests/guestPermissions.test.ts, блокGuest: запрет без уровня доступа для гостей,DerivedTxпроверяется как обычная tx, в контексте триггера проходит.foundations/server/packages/middleware/src/__tests__/seatLimits.test.ts: участник без места остаётся только для чтения приDerivedTx, в контексте триггера его tx проходит.
На коде до исправления новые тесты на подделку падают.
pnpm build:lint --to @hcengineering/middlewarecd foundations/core/packages/core && npx jest src/__tests__/objvalue.test.tscd foundations/server/packages/middleware && npx jest src/tests/normalizeTx.test.ts src/tests/spaceSecurity.test.ts src/tests/guestPermissions.test.ts src/__tests__/seatLimits.test.tsСвязанное: space_security_broadcasts.md.