feat(security): sandbox the renderer (contextIsolation, nodeIntegration: false, sandbox: true)#4244
Merged
Merged
Conversation
…on: false, sandbox: true) The editor and preferences BrowserWindows now run with the full Chromium sandbox: contextIsolation: true, nodeIntegration: false, sandbox: true. A renderer compromise can no longer reach fs, child_process, ripgrep, picgo, or raw Electron modules — every Node-side operation routes through a narrow contextBridge surface to ~30 new mt:: IPC handlers in src/main/ipc/. @electron/remote is removed entirely. Renderer-side Node imports are gone: fileSystem.js uses Web Crypto for hashing and IPC for everything else; ripgrep/file searchers become thin IPC bridges; muya/plantuml swaps zlib → pako; muya/utils swaps path → path-browserify (aliased in Vite); muya/imageCtrl replaces Node's url helper with an inline browser-safe one. vite-plugin-electron-renderer is dropped so any new stray Node import fails the build loudly. New test/e2e/context-isolation.spec.js asserts the boundary stays live. pnpm run lint, pnpm run test:unit (550/550), and pnpm run build:unpack all pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
This PR hardens MarkText’s Electron renderer security boundary by enabling Chromium sandboxing (contextIsolation: true, nodeIntegration: false, sandbox: true) and migrating renderer-side Node/Electron access to a contextBridge + main-process IPC surface (including removal of @electron/remote).
Changes:
- Add a sandbox-safe preload exposing limited APIs (
window.electron,window.fileUtils,window.ripgrep, etc.) and implement corresponding main-process IPC handlers undersrc/main/ipc/. - Migrate renderer features (filesystem, uploads, clipboard, menus, ripgrep search, fonts, i18n, window controls) from direct Node/remote usage to IPC-backed shims.
- Add an e2e Playwright test to assert renderer sandboxing invariants and prevent regressions.
Reviewed changes
Copilot reviewed 55 out of 56 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| test/e2e/context-isolation.spec.js | Adds e2e canary assertions for sandboxing + preload leakage. |
| src/renderer/src/util/pdf.js | Loads export theme CSS via IPC-backed file reads (async). |
| src/renderer/src/util/index.js | Switches platform detection to boot-info exposed platform. |
| src/renderer/src/util/fileSystem.js | Replaces Node crypto/fs/child_process usage with WebCrypto + IPC. |
| src/renderer/src/util/clipboard.js | Switches clipboard file-path guessing to IPC bridge (async). |
| src/renderer/src/store/project.js | Removes direct process.env usage in renderer. |
| src/renderer/src/store/preferences.js | Migrates global.marktext → window.marktext. |
| src/renderer/src/store/layout.js | Migrates global.marktext → window.marktext. |
| src/renderer/src/store/editor.js | Migrates global.marktext → window.marktext. |
| src/renderer/src/prefComponents/keybindings/index.vue | Migrates global.marktext → window.marktext. |
| src/renderer/src/prefComponents/image/components/uploader/index.vue | Makes CLI-script executable check async via IPC. |
| src/renderer/src/prefComponents/common/titlebar.vue | Replaces @electron/remote window close with IPC window control. |
| src/renderer/src/prefComponents/common/fontTextBox/index.vue | Moves font enumeration to main-process IPC. |
| src/renderer/src/pages/preference.vue | Migrates global.marktext → window.marktext. |
| src/renderer/src/pages/app.vue | Migrates global.marktext → window.marktext. |
| src/renderer/src/node/ripgrepSearcher.js | Replaces renderer spawn(rg) with IPC-streamed ripgrep wrapper. |
| src/renderer/src/node/paths.js | Uses boot-info exposed paths/env instead of Node process. |
| src/renderer/src/node/fileSearcher.js | Delegates file search to IPC-backed FileSearcher. |
| src/renderer/src/main.js | Migrates global.marktext → window.marktext. |
| src/renderer/src/i18n/index.js | Loads translations via IPC + dedupes in-flight loads (async). |
| src/renderer/src/contextMenu/tabs/index.js | Replaces remote menu popup with serialized IPC menu popup. |
| src/renderer/src/contextMenu/sideBar/index.js | Replaces remote menu popup with serialized IPC menu popup. |
| src/renderer/src/contextMenu/popupMenu.js | New renderer helper for serializing menus + dispatching clicks over IPC. |
| src/renderer/src/components/titleBar/index.vue | Replaces remote window/menu calls with IPC window control + cleans listeners. |
| src/renderer/src/components/exportSettings/index.vue | Loads export themes via IPC-backed filesystem access. |
| src/renderer/src/components/editorWithTabs/editor.vue | Adapts export CSS + Unsplash key + clipboard path for sandboxed renderer. |
| src/renderer/src/commands/utils.js | Makes updatable check async via IPC file checks. |
| src/renderer/src/commands/quickOpen.js | Migrates global.marktext → window.marktext. |
| src/renderer/src/commands/index.js | Replaces remote window calls and async-gates update command registration. |
| src/renderer/src/bootstrap.js | Migrates global.marktext → window.marktext and uses boot-info env. |
| src/preload/index.js | Rewrites preload to contextBridge + IPC-only APIs; adds boot-info sync handshake. |
| src/muya/lib/utils/turndownService.js | Converts require to ESM import for sandbox-friendly bundling. |
| src/muya/lib/utils/index.js | Switches Node path import to path-browserify. |
| src/muya/lib/parser/render/snabbdom.js | Converts require('snabbdom-to-html') to ESM import. |
| src/muya/lib/parser/render/plantuml.js | Replaces Node zlib/Buffer encoding with pako + browser-safe base64. |
| src/muya/lib/contentState/pasteCtrl.js | Supports async clipboard file-path provider. |
| src/muya/lib/contentState/imageCtrl.js | Adds browser-safe fileURLToPath replacement. |
| src/muya/lib/assets/libs/sequence-diagram-snap.js | Strips unreachable CLI bootstrap that breaks sandboxed bundling. |
| src/main/windows/setting.js | Removes @electron/remote enablement. |
| src/main/windows/editor.js | Removes @electron/remote enablement. |
| src/main/ipc/window.js | Adds IPC handlers for window control + menu popup plumbing. |
| src/main/ipc/uploader.js | Adds IPC image uploader implementation (PicGo/CLI/GitHub). |
| src/main/ipc/shell.js | Adds IPC handlers for shell + clipboard bridging. |
| src/main/ipc/ripgrep.js | Adds IPC ripgrep/file-search implementation with streaming events + cancellation. |
| src/main/ipc/paths.js | Adds IPC handlers for filesystem path helpers. |
| src/main/ipc/index.js | Registers the sandbox IPC handler suite. |
| src/main/ipc/i18n.js | Adds IPC handlers for loading i18n translations. |
| src/main/ipc/fs.js | Adds IPC filesystem handlers (read/write/stat/copy/move/etc.). |
| src/main/ipc/fonts.js | Adds IPC font enumeration using font-list in main process. |
| src/main/ipc/cmd.js | Adds IPC handler for command-exists checks. |
| src/main/ipc/bootInfo.js | Adds boot-info sync/async endpoints with allowlisted env + paths. |
| src/main/index.js | Removes remote initialization and registers sandbox IPC handlers. |
| src/main/config.js | Enables sandboxed renderer flags in BrowserWindow webPreferences. |
| pnpm-lock.yaml | Removes @electron/remote and adds pako/path-browserify/plist dependencies. |
| package.json | Removes @electron/remote, drops vite-plugin-electron-renderer, adds pako/path-browserify/plist. |
| electron.vite.config.js | Removes vite-plugin-electron-renderer; aliases path to path-browserify; inlines preload deps. |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…-party deps CI regression cause: 22 e2e tests hung waiting for `.editor-component` because @hfelix/electron-localshortcut/src/utils.js reads `process.platform` at module load — undefined in a sandboxed renderer, which threw `ReferenceError: process is not defined` from `requireConvertAccelerator` and stopped Vue from mounting. Expose a minimal `process` shim via contextBridge so libraries that do this keep working without re-opening Node access. Also addresses Copilot inline review: - isChildOfDirectory / hasMarkdownExtension / isSamePathSync now run synchronously in the preload (pure path-string ops via path-browserify), with a sendSync fallback only for the case-insensitive isSamePathSync branch. Fixes the tab/file-matching breakage where `tabs.find(t => isSamePathSync(...))` was finding a truthy Promise. - Drop the misleading ensureDirSync / pathExistsSync names — no callers. - isUpdatable() goes synchronous via a boot-info flag computed once by main, so `file.check-update` is registered in the correct phase with a description, not racily pushed later. - ANSI SGR strip in parsePicgoOutput uses an explicit \x1b escape via a named regex instead of a literal ESC byte. - mt::menu::popup deletes its `popups` map entry on synchronous failure to prevent a stale-sender leak. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Six new specs that exercise the IPC contracts introduced when the renderer went sandboxed. They protect the surfaces that broke during the migration and would have caught the CI regression from PR #4244. - window-controls: mt::win::is-maximized / is-fullscreen invokes, toggle IPC matches BrowserWindow state, and the windowControl bridge surface is fully exposed. - plantuml: a fenced ```plantuml block renders an <img> whose src matches plantuml.com/plantuml/svg/~1... — guards the pako-based encoder that replaced Node zlib in muya. - shell-external: stubs shell.openExternal in main and verifies window.electron.shell.openExternal forwards the URL. - font-list: window.fonts.list resolves to an array via mt::fonts::list (font-list runs in main now). - ripgrep-search: writes a small fixture tree and drives window.ripgrep.start in both 'text' and 'files' modes, asserting the streaming match/done events arrive. - image-drop: exercises the image-drop primitives end-to-end (readFile → Web Crypto SHA-1 → ensureDir → copy) and checks the content-addressed output file lands at the hashed path. 61 e2e tests pass locally (52 → 61, +9). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This reverts commit 182b7ab.
Two focused regression nets for the sandboxing migration: - plantuml.spec.js: validates the pako-based encoder that replaced Node zlib in src/muya/lib/parser/render/plantuml.js. A fenced ```plantuml block must render an <img> whose src matches plantuml.com/plantuml/svg/~1<base64-table-translated>. Catches silent regressions in pako/base64/maketrans that wouldn't surface in code review. - ripgrep-search.spec.js: drives window.ripgrep.start in both 'text' and 'files' modes against a fixture tree, and asserts streamed match/done events arrive. Covers the most complex IPC pattern introduced by the migration (multi-directory streaming + cancellation via searchId). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… IPC Two follow-ups surfaced while smoke-testing the packaged Mac DMG and the sidebar search dev flow. - Inline @octokit/rest into the main bundle via externalizeDeps.exclude. After moving the GitHub uploader to main, electron-builder's asar packing + pnpm's flattened node_modules dropped @octokit/endpoint (transitive), producing ERR_MODULE_NOT_FOUND at app launch in the packaged build. Bundling the dep removes runtime resolution of the transitive graph. - Add `global: 'globalThis'` to the renderer Vite/esbuild define so custom-event (via dragula → crossvent) doesn't throw ReferenceError on module load in the sandboxed renderer. - JSON round-trip ripgrep search options before window.ripgrep.start. Pinia hands the searcher reactive Proxies via storeToRefs().value; structured clone over IPC rejects them with "An object could not be cloned". Stripping to plain JSON values resolves it without forcing every call site to deep-clone its inputs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
7 tasks
Jocs
added a commit
that referenced
this pull request
May 22, 2026
Keep only the substantive TS-migration content edits in CLAUDE.md: - Add \`Language | TypeScript 5.9 (strict mode)\` row to Tech Stack - Update preload note (renderer is sandboxed since #4244) - Add \`pnpm run typecheck\` to Testing - Update Code Style with TS conventions The Stage 6 prettier pass had also expanded the Tech Stack table columns with whitespace alignment; restore the original compact form per "don't format docs" guidance.
Jocs
added a commit
that referenced
this pull request
May 22, 2026
Commit 10 of 12. - New \`docs/dev/TYPESCRIPT.md\` covering: tsconfig layout, path aliases, type placement (src/shared/types vs src/types vs co-located), the IPC contract pattern, the muya boundary, TypedEmitter for main classes, Pinia Setup Store conventions, strict-mode landmines, and the list of deferred type-tightening work. - Root \`CLAUDE.md\`: fixed outdated preload note (renderer is sandboxed since #4244 — contextIsolation: true, nodeIntegration: false), added Language row to Tech Stack table, added \`pnpm run typecheck\` to the Testing block, refreshed Code Style with TS conventions. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Lock the editor and preferences
BrowserWindows into the full Chromium sandbox:contextIsolation: true,nodeIntegration: false,sandbox: true. A renderer compromise (untrusted markdown, image embeds, math/Mermaid input, etc.) can no longer reachfs,child_process, ripgrep, picgo, or raw Electron modules — every Node-side operation routes through a narrowcontextBridgesurface to ~30 newmt::*IPC handlers insrc/main/ipc/.@electron/remoteis removed entirely.src/main/ipc/(new): boot info, fs, paths, ripgrep streaming, picgo/cliScript/github uploader, font enumeration, shell + clipboard shims, window control + menu popup, command-exists, i18n.electronisrequire()d at runtime;path-browserifyis inlined viaexternalizeDeps.excludeinelectron.vite.config.js.@electron/remoteremoved: 6 renderer files migrated to IPC-based window control + serialized menu popup template flow.util/fileSystem.jsuses Web Crypto for hashing and IPC for everything else; ripgrep/file searchers become thin IPC bridges;global.marktext→window.marktext(11 files).plantuml.jszlib → pako;utils/index.jspath → path-browserify (Vite alias);turndownService.js/snabbdom.jsrequire→import;imageCtrl.jsNodeurl.fileURLToPath→ inline browser-safe helper; deadrequire('fs')insequence-diagram-snap.jsstripped.vite-plugin-electron-rendererdropped so any future stray Node import fails the build loudly.Test plan
pnpm run lint— cleanpnpm run test:unit— 550/550 passpnpm run build:unpack— preload 19.94 kB, renderer builds cleanpnpm exec playwright test test/e2e/context-isolation.spec.js— assertions pass (contextBridgelive,require/global/Bufferundefined, no preload-scope leakage). TheafterAllapp.close()timeout reproduces on the pre-existinglaunch.spec.jstoo and is not introduced by these changes.pnpm run dev:MaxListenersExceededWarning)mt::fs::*)mt::fs::read-file)mt::shell::open-external)pnpm run build:linux|mac|win) to catch native-module asar-unpack regressions for@vscode/ripgrep,font-list,keytar.🤖 Generated with Claude Code