[bug][ui] Автотест стенда (проход #2): share-not-bound-to-shareId, new-page-empty-body, editor-read-only-window, breadcrumb-lag, page-info-leak, callout-paste #218

Closed
opened 2026-06-26 15:55:38 +03:00 by Ghost · 0 comments

Второй проход автономного тестирования (web-test-orchestrator, прогон против ПЕРЕСОБРАННОГО feat/git-sync с фиксами data-loss). Только гитмост-UI баги. git-sync дублирование — починено в PR #119 (см. отдельный отчёт-ишью). Дубли прошлого прохода (#216) и уже-исправленное помечены.

[medium] Доступ к содержимому публичной share-страницы не привязан к shareId — поддельный/невалидный shareId всё равно отрисовывает полную share-страницу

Воспроизведение: В свежем инкогнито-контексте откройте http://localhost:5173/share/doesnotexist99/p/topcause-4242-hA5kn4vwqQ (настоящий slug расшаренной страницы, выдуманный shareId). Страница отрисовывается полностью. Перекрёстная проверка: POST /api/shares/tree {"shareId":"doesnotexist99"} -> 404 'Share not found', но POST /api/shares/page-info {"pageId":"hA5kn4vwqQ"} (без shareId) -> 200 с полным содержимым.

Доказательство: Verifier воспроизвёл: tree API отдаёт 404 для поддельного shareId, тогда как page-info отвечает 200 и возвращает страницу целиком (тело TOPCAUSE4242). Первопричина в apps/server/src/core/share/share.service.ts getSharedPage() (~L191-211) вызывает resolveReadableSharePage(null, dto.pageId, ...) — shareId намеренно передаётся null, поэтому доступ к содержимому ограничен лишь случайным 10-символьным slugId страницы и проверкой «расшарена ли вообще эта страница». Клиент apps/client/src/pages/share/shared-page.tsx (L25-27) запрашивает page-info только по slugId. Более того, useEffect (L32-45) редиректит несовпадающий shareId на канонический URL, ТЕМ САМЫМ УТЕКАЯ настоящий share-ключ тому, кто угадал только slug. Per-link секрет, показываемый в диалоге Share, не является границей доступа и не может быть ротирован/отозван без снятия шаринга. Эксплуатируемость ограничена (slugId — случайный nanoid, который атакующий должен знать); вероятно, унаследованное поведение upstream Docmost.

[medium] Новая страница молча сохраняет пустое тело, если контент набран до подключения Yjs/collab-провайдера (потеря данных)

Воспроизведение: Создайте страницу, сразу начните набирать тело (до подключения collab-websocket), затем перезагрузите или уйдите со страницы. Тело сохраняется пустым.

Доказательство: Verifier подтвердил через git-export gold standard на свежем клоне: 'MEARLY 72474.md' содержит только frontmatter, хотя заголовок сохранился; тела FASTMARK/SLOWMARK отсутствуют во всех 108+ md-файлах; контрольные образцы с 7s паузой перед набором сохранились полностью (VCTRL, VPASTE). В момент захвата потерянные тела БЫЛИ в живом ProseMirror DOM. Реальный триггер скорректирован относительно гипотезы репортёра: это НЕ синтетическая вставка (VPASTE сохранился) и НЕ навигация (MEARLY потерял тело без навигации + 12s паузы) — это набор текста в только что созданную collab-страницу до подключения Yjs-провайдера (ws://...:3001/collab), так что нажатия клавиш попадают только в локальный ProseMirror и никогда не доходят до сохраняемого shared doc. Ноль ошибок в консоли. Требует редактирования только что созданной страницы в пределах sub-7s окна подключения; более медленное редактирование существующих страниц безопасно.

[low] Редактор read-only (contenteditable=false) ~5s после открытия; правки, набранные в этом окне, молча отбрасываются

Воспроизведение: Откройте страницу через /p/ и сразу (до подключения collab-websocket) кликните по абзацу и наберите текст.

Доказательство: Verifier подтвердил в исходниках apps/client/src/features/editor/page-editor.tsx: showStatic=useState(true) (L458) переключается в false только когда Yjs Connected + isLocalSynced + isRemoteSynced (L460-469); во время showStatic рендерится EditorProvider с editable=false, показывающий кэшированный контент (L475-486), а редактируемый редактор монтируется только после синхронизации (L487-490). Набор текста в статической фазе попадает в editable=false редактор и затем заменяется синхронизированным doc. Молчаливый аспект подтверждён: page-header-menu.tsx (L383-428) показывает лишь иконку wifi-off 'connection lost' после 5000ms дисконнекта — никакого read-only/loading-индикатора в первые секунды. Механизм подтверждён кодом; живой образец contenteditable=false не захвачен, потому что медленный dev-mount происходит уже после синхронизации (оговорка инструментария).

[low] Breadcrumb на глубоких страницах часто пустой несколько секунд (отстаёт от основного контента)

Воспроизведение: Откройте страницу 3-го уровня вложенности напрямую по URL (например, TN Linger > TN Child B > TN Mover). Следите за breadcrumb в шапке страницы; перезагрузите несколько раз.

Доказательство: Verifier воспроизвёл: в окне 8s путь из >=2 сегментов появился лишь на 1/5 загрузок (5705ms); в окне 25s он появился на 9204ms/12735ms, т.е. на 2358ms и 8539ms ПОЗЖЕ, чем отрендерился H1 редактора — то есть breadcrumb специфически отстаёт от контента. НОЛЬ ошибок в консоли и НОЛЬ HTTP 4xx/5xx за период пустоты; когда заполняется, путь корректен с настоящими -ссылками. Первопричина в apps/client/src/features/page/components/breadcrumbs/breadcrumb.tsx (L43-48): useEffect защищён условием treeData?.length > 0 && currentPage и вызывает findBreadcrumbPath(treeData, currentPage.id); treeData — это atom дерева сайдбара, заполняемый лениво/независимо от page-запроса, поэтому пока дерево не содержит цепочку предков,

рендерится пустым. Vite-dev миллисекунды раздуты, но устойчивое относительное отставание (breadcrumb позади H1) реально. Низкая важность: всегда в итоге рендерится корректно.

[low] Публичный page-info endpoint раскрывает внутренние метаданные (user/space/workspace ID) анонимным зрителям

Воспроизведение: Без аутентификации: curl -X POST http://localhost:3000/api/shares/page-info -H 'Content-Type: application/json' -d '{"pageId":"hA5kn4vwqQ"}'

Доказательство: Verifier воспроизвёл HTTP 200, чей объект data.page раскрывает creatorId, lastUpdatedById, contributorIds[], spaceId, workspaceId, lastUpdatedAiChatId, lastUpdatedSource, isLocked, isTemplate, parentPageId, position, createdAt/updatedAt/deletedAt; data.share дополнительно повторно утекает creatorId/spaceId/workspaceId. Это действительно публичный share-путь (валидный share-ключ присутствует), а не обход авторизации. Влияние незначительное — все непрозрачные UUID, без PII/секретов — но больше, чем нужно публичному рендереру.

[low] Вставка markdown-синтаксиса callout > [!info] в редактор даёт обычный blockquote с буквальным текстом '[!info]'

Воспроизведение: В теле UI-страницы вставьте plain-text markdown, содержащий > [!info]\n> body (например, через буфер обмена).

Доказательство: Verifier воспроизвёл: вставленный блок отрендерился как

[!info]
Callout body text here

(количество callout-нод 0), тогда как heading/вложенный список/code-fence в той же вставке сконвертировались корректно. Подтверждено юнит-тестом: markdownToHtml('> [!info]...') => blockquote с буквальным [!info]; markdownToHtml(':::info...') => корректный callout div. Первопричина в packages/editor-ext/src/lib/markdown/utils/callout.marked.ts: правило токенизатора /^:::([a-zA-Z0-9]+)\s+([\s\S]+?):::/ распознаёт только форму :::type ... :::, но не GitHub > [!type]. Путь вставки (apps/client/src/features/editor/extensions/markdown-clipboard.ts handlePaste) проходит через тот же markdownToHtml. ПРИМЕЧАНИЕ: касается только in-editor-вставки — путь git-ИМПОРТА конвертирует > [!type] корректно (отдельный code path, проверено положительно), так что это побочно к git-sync. Без краша/потери данных; остаётся редактируемым blockquote.

Второй проход автономного тестирования (web-test-orchestrator, прогон против ПЕРЕСОБРАННОГО feat/git-sync с фиксами data-loss). Только **гитмост-UI** баги. git-sync дублирование — починено в PR #119 (см. отдельный отчёт-ишью). Дубли прошлого прохода (#216) и уже-исправленное помечены. ## [medium] Доступ к содержимому публичной share-страницы не привязан к shareId — поддельный/невалидный shareId всё равно отрисовывает полную share-страницу **Воспроизведение:** В свежем инкогнито-контексте откройте http://localhost:5173/share/doesnotexist99/p/topcause-4242-hA5kn4vwqQ (настоящий slug расшаренной страницы, выдуманный shareId). Страница отрисовывается полностью. Перекрёстная проверка: POST /api/shares/tree {"shareId":"doesnotexist99"} -> 404 'Share not found', но POST /api/shares/page-info {"pageId":"hA5kn4vwqQ"} (без shareId) -> 200 с полным содержимым. **Доказательство:** Verifier воспроизвёл: tree API отдаёт 404 для поддельного shareId, тогда как page-info отвечает 200 и возвращает страницу целиком (тело TOPCAUSE4242). Первопричина в apps/server/src/core/share/share.service.ts getSharedPage() (~L191-211) вызывает resolveReadableSharePage(null, dto.pageId, ...) — shareId намеренно передаётся null, поэтому доступ к содержимому ограничен лишь случайным 10-символьным slugId страницы и проверкой «расшарена ли вообще эта страница». Клиент apps/client/src/pages/share/shared-page.tsx (L25-27) запрашивает page-info только по slugId. Более того, useEffect (L32-45) редиректит несовпадающий shareId на канонический URL, ТЕМ САМЫМ УТЕКАЯ настоящий share-ключ тому, кто угадал только slug. Per-link секрет, показываемый в диалоге Share, не является границей доступа и не может быть ротирован/отозван без снятия шаринга. Эксплуатируемость ограничена (slugId — случайный nanoid, который атакующий должен знать); вероятно, унаследованное поведение upstream Docmost. ## [medium] Новая страница молча сохраняет пустое тело, если контент набран до подключения Yjs/collab-провайдера (потеря данных) **Воспроизведение:** Создайте страницу, сразу начните набирать тело (до подключения collab-websocket), затем перезагрузите или уйдите со страницы. Тело сохраняется пустым. **Доказательство:** Verifier подтвердил через git-export gold standard на свежем клоне: 'MEARLY 72474.md' содержит только frontmatter, хотя заголовок сохранился; тела FASTMARK/SLOWMARK отсутствуют во всех 108+ md-файлах; контрольные образцы с 7s паузой перед набором сохранились полностью (VCTRL, VPASTE). В момент захвата потерянные тела БЫЛИ в живом ProseMirror DOM. Реальный триггер скорректирован относительно гипотезы репортёра: это НЕ синтетическая вставка (VPASTE сохранился) и НЕ навигация (MEARLY потерял тело без навигации + 12s паузы) — это набор текста в только что созданную collab-страницу до подключения Yjs-провайдера (ws://...:3001/collab), так что нажатия клавиш попадают только в локальный ProseMirror и никогда не доходят до сохраняемого shared doc. Ноль ошибок в консоли. Требует редактирования только что созданной страницы в пределах sub-7s окна подключения; более медленное редактирование существующих страниц безопасно. ## [low] Редактор read-only (contenteditable=false) ~5s после открытия; правки, набранные в этом окне, молча отбрасываются **Воспроизведение:** Откройте страницу через /p/<id> и сразу (до подключения collab-websocket) кликните по абзацу и наберите текст. **Доказательство:** Verifier подтвердил в исходниках apps/client/src/features/editor/page-editor.tsx: showStatic=useState(true) (L458) переключается в false только когда Yjs Connected + isLocalSynced + isRemoteSynced (L460-469); во время showStatic рендерится EditorProvider с editable=false, показывающий кэшированный контент (L475-486), а редактируемый редактор монтируется только после синхронизации (L487-490). Набор текста в статической фазе попадает в editable=false редактор и затем заменяется синхронизированным doc. Молчаливый аспект подтверждён: page-header-menu.tsx (L383-428) показывает лишь иконку wifi-off 'connection lost' после 5000ms дисконнекта — никакого read-only/loading-индикатора в первые секунды. Механизм подтверждён кодом; живой образец contenteditable=false не захвачен, потому что медленный dev-mount происходит уже после синхронизации (оговорка инструментария). ## [low] Breadcrumb на глубоких страницах часто пустой несколько секунд (отстаёт от основного контента) **Воспроизведение:** Откройте страницу 3-го уровня вложенности напрямую по URL (например, TN Linger > TN Child B > TN Mover). Следите за breadcrumb в шапке страницы; перезагрузите несколько раз. **Доказательство:** Verifier воспроизвёл: в окне 8s путь из >=2 сегментов появился лишь на 1/5 загрузок (5705ms); в окне 25s он появился на 9204ms/12735ms, т.е. на 2358ms и 8539ms ПОЗЖЕ, чем отрендерился H1 редактора — то есть breadcrumb специфически отстаёт от контента. НОЛЬ ошибок в консоли и НОЛЬ HTTP 4xx/5xx за период пустоты; когда заполняется, путь корректен с настоящими <a>-ссылками. Первопричина в apps/client/src/features/page/components/breadcrumbs/breadcrumb.tsx (L43-48): useEffect защищён условием treeData?.length > 0 && currentPage и вызывает findBreadcrumbPath(treeData, currentPage.id); treeData — это atom дерева сайдбара, заполняемый лениво/независимо от page-запроса, поэтому пока дерево не содержит цепочку предков, <nav> рендерится пустым. Vite-dev миллисекунды раздуты, но устойчивое относительное отставание (breadcrumb позади H1) реально. Низкая важность: всегда в итоге рендерится корректно. ## [low] Публичный page-info endpoint раскрывает внутренние метаданные (user/space/workspace ID) анонимным зрителям **Воспроизведение:** Без аутентификации: curl -X POST http://localhost:3000/api/shares/page-info -H 'Content-Type: application/json' -d '{"pageId":"hA5kn4vwqQ"}' **Доказательство:** Verifier воспроизвёл HTTP 200, чей объект data.page раскрывает creatorId, lastUpdatedById, contributorIds[], spaceId, workspaceId, lastUpdatedAiChatId, lastUpdatedSource, isLocked, isTemplate, parentPageId, position, createdAt/updatedAt/deletedAt; data.share дополнительно повторно утекает creatorId/spaceId/workspaceId. Это действительно публичный share-путь (валидный share-ключ присутствует), а не обход авторизации. Влияние незначительное — все непрозрачные UUID, без PII/секретов — но больше, чем нужно публичному рендереру. ## [low] Вставка markdown-синтаксиса callout `> [!info]` в редактор даёт обычный blockquote с буквальным текстом '[!info]' **Воспроизведение:** В теле UI-страницы вставьте plain-text markdown, содержащий `> [!info]\n> body` (например, через буфер обмена). **Доказательство:** Verifier воспроизвёл: вставленный блок отрендерился как <blockquote><p>[!info]<br>Callout body text here</p></blockquote> (количество callout-нод 0), тогда как heading/вложенный список/code-fence в той же вставке сконвертировались корректно. Подтверждено юнит-тестом: markdownToHtml('> [!info]...') => blockquote с буквальным [!info]; markdownToHtml(':::info...') => корректный callout div. Первопричина в packages/editor-ext/src/lib/markdown/utils/callout.marked.ts: правило токенизатора /^:::([a-zA-Z0-9]+)\s+([\s\S]+?):::/ распознаёт только форму `:::type ... :::`, но не GitHub `> [!type]`. Путь вставки (apps/client/src/features/editor/extensions/markdown-clipboard.ts handlePaste) проходит через тот же markdownToHtml. ПРИМЕЧАНИЕ: касается только in-editor-вставки — путь git-ИМПОРТА конвертирует `> [!type]` корректно (отдельный code path, проверено положительно), так что это побочно к git-sync. Без краша/потери данных; остаётся редактируемым blockquote.
vvzvlad added the bug label 2026-06-26 21:01:13 +03:00
Ghost closed this issue 2026-06-28 03:43:28 +03:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: vvzvlad/gitmost#218