From 8de7e3720fa8857d8e9e7bef8d862b3131bbe412 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 18 Aug 2026 02:02:47 +0000 Subject: [PATCH] docs: add 2026-08-18 Electron/Next frontend audit Read-only review of preload/IPC, chat stop UX, session delete/drafts, XSS surfaces, knowledge paging, and the frontend-ci red on cfccbbb. Co-authored-by: wu1w --- docs/audit-frontend-2026-08-18.md | 317 ++++++++++++++++++++++++++++++ 1 file changed, 317 insertions(+) create mode 100644 docs/audit-frontend-2026-08-18.md diff --git a/docs/audit-frontend-2026-08-18.md b/docs/audit-frontend-2026-08-18.md new file mode 100644 index 00000000..e4066888 --- /dev/null +++ b/docs/audit-frontend-2026-08-18.md @@ -0,0 +1,317 @@ +# Frontend audit — Electron / Next.js (read-only) + +**Audited SHA:** `cfccbbb9ed3a8a489097aa3a6e42ee0049a66e43` (`cfccbbb` on `main`) +**Date:** 2026-08-18 +**Scope:** Desktop Electron shell + `frontend/` Next.js UI. No product or CI files were changed. +**Method:** Independent code review of preload/IPC, chat stop/end, session delete + drafts, markdown/HTML rendering, knowledge paging, settings props, and GitHub Actions `frontend-ci` logs. Root cause was not assumed. + +Neighbors on `main` that matter for this report: + +| SHA | Title | frontend-ci | +|---|---|---| +| `4133b5f` | Unify Rust court authority and make chat stop/end UX match mainstream agents | **green** | +| `698ebe5` | …land chat inspector composer | **red** | +| `cfccbbb` | Fix audit P1/P2 chat runtime gaps: session delete, draft restore, tool stop… | **red** | + +--- + +## frontend-ci: why latest main is red + +**Gate that fails:** `frontend` job `ESLint (gate)` (`npm run lint` in `frontend/`). Typecheck and Next build never run. + +**Latest failed run (this SHA):** +https://github.com/wu1w/tevarn/actions/runs/32089209073 +(`cfccbbb`, 2026-08-18T01:43Z, ~1m2s) + +**Previous failed run (first red after a green main):** +https://github.com/wu1w/tevarn/actions/runs/32037918795 +(`698ebe5`, 2026-08-17T14:07Z) + +**Last green `frontend-ci` on main:** +https://github.com/wu1w/tevarn/actions/runs/32004377076 +(`4133b5f`, 2026-08-17T07:06Z) + +### Exact blocking error + +`eslint-plugin-react-hooks` **7.1.1** (pulled by `eslint-config-next` 16.2.10) reports **1 error, 133 warnings**. Warnings do not fail the job. The single error does: + +``` +frontend/hooks/useColResize.ts:23 + error Cannot access refs during render / Cannot update ref during render + react-hooks/refs + + const widthRef = useRef(width); + widthRef.current = width; +``` + +`useColResize.ts` was **added in `698ebe5`** (composer / inspector column resize). `cfccbbb` did not touch this file. That is why `4133b5f` was green and the next two main pushes are red. + +`frontend/eslint.config.mjs` downgrades several React Compiler rules (`set-state-in-effect`, `static-components`, `immutability`, `purity`) to **warn**. It does **not** downgrade `react-hooks/refs`, so the new hook is a hard gate. + +### Suggested fix (do not apply in this PR) + +In `frontend/hooks/useColResize.ts`, stop writing the ref during render. Either: + +1. Sync in an effect: `useEffect(() => { widthRef.current = width; }, [width]);` (one extra frame of staleness is fine — `onStart` reads the ref only on pointer down), or +2. Read `width` from a state updater / pass it into `onStart` so the ref is unnecessary. + +Do **not** silence `react-hooks/refs` globally. The 133 warnings are noise, not the outage. + +--- + +## Findings + +### Critical + +None observed that are exploitable as a remote, unauthenticated RCE from the renderer **given** `contextIsolation: true`, `nodeIntegration: false`, and `sandbox: true`. Remaining IPC and HTML-rendering issues are High/Medium. + +--- + +### High + +#### H1 — `sync_response` idle path drops the in-flight partial (stop/end race) + +**Files:** `frontend/app/chat/page.tsx` (`handleSyncResponse` ~1044–1053 vs `keepPartialAssistantOnIdle` ~89–126 and status `idle` ~813–853) + +`status: idle` and the local Stop 8s fallback call `keepPartialAssistantOnIdle`, which reloads history and, if the leftover is not yet in the store, inserts a local assistant bubble (ChatGPT/Cursor-style). + +`handleSyncResponse` when `agent_running` is false does **not**. It clears `streamingContent` / `liveToolCalls` and only `loadMessages`. If the WS sync wins the race before the backend has persisted the assistant row, the user sees an empty stop: no partial, no in-flight tools. + +This is the most likely “I hit Stop and the reply vanished” path after `4133b5f` / `cfccbbb`. Those commits hardened the **status** path, not the **sync** path. + +**Suggested fix:** On sync-idle, reuse `keepPartialAssistantOnIdle(sid, leftover, loadMessages, addMessage)` before clearing refs. Treat sync as another idle signal, not a hard wipe. + +#### H2 — File-preview markdown skips the chat URL allow-list + +**Files:** `frontend/components/chat/FilePreviewHost.tsx` (~480–483), vs `frontend/components/chat/MarkdownContent.tsx` (`safeUrlTransform`, ~10–18, ~123–128) + +Chat bubbles use `react-markdown` **without** `rehype-raw`, plus `urlTransform={safeUrlTransform}` (only `http(s)`, `mailto:`, `data:image/`, relative `./#?`). e2e `frontend/e2e/brutal-ui.spec.ts` probes `javascript:` and `