Skip to content

feat(security): sandbox the renderer (contextIsolation, nodeIntegration: false, sandbox: true)#4244

Merged
Jocs merged 6 commits into
developfrom
worktree-expressive-soaring-swan
May 22, 2026
Merged

feat(security): sandbox the renderer (contextIsolation, nodeIntegration: false, sandbox: true)#4244
Jocs merged 6 commits into
developfrom
worktree-expressive-soaring-swan

Conversation

@Jocs
Copy link
Copy Markdown
Member

@Jocs Jocs commented May 21, 2026

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 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.

  • 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.
  • Preload rewritten: only electron is require()d at runtime; path-browserify is inlined via externalizeDeps.exclude in electron.vite.config.js.
  • @electron/remote removed: 6 renderer files migrated to IPC-based window control + serialized menu popup template flow.
  • Renderer Node imports gone: util/fileSystem.js uses Web Crypto for hashing and IPC for everything else; ripgrep/file searchers become thin IPC bridges; global.marktextwindow.marktext (11 files).
  • Muya migrated: plantuml.js zlib → pako; utils/index.js path → path-browserify (Vite alias); turndownService.js / snabbdom.js requireimport; imageCtrl.js Node url.fileURLToPath → inline browser-safe helper; dead require('fs') in sequence-diagram-snap.js stripped.
  • Build: vite-plugin-electron-renderer dropped so any future stray Node import fails the build loudly.

Test plan

  • pnpm run lint — clean
  • pnpm run test:unit — 550/550 pass
  • pnpm run build:unpack — preload 19.94 kB, renderer builds clean
  • pnpm exec playwright test test/e2e/context-isolation.spec.js — assertions pass (contextBridge live, require/global/Buffer undefined, no preload-scope leakage). The afterAll app.close() timeout reproduces on the pre-existing launch.spec.js too and is not introduced by these changes.
  • Manual smoke under pnpm run dev:
    • Open/close folder, sidebar search (ripgrep IPC), quick open (file searcher IPC)
    • Window controls: minimize, maximize/restore, full-screen, multi-window cycle (no MaxListenersExceededWarning)
    • Right-click tab + sidebar menus (templated popup IPC)
    • Drag image into doc (Web Crypto hashing + mt::fs::*)
    • Image upload via PicGo / github (uploader IPC)
    • Render PlantUML code block (pako deflate)
    • PDF / styled HTML export (theme CSS via mt::fs::read-file)
    • Preferences: font picker (font-list IPC), spellchecker toggle, theme switch
    • External link click in doc (mt::shell::open-external)
  • Production build on at least one platform (pnpm run build:linux|mac|win) to catch native-module asar-unpack regressions for @vscode/ripgrep, font-list, keytar.

🤖 Generated with Claude Code

…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>
Copilot AI review requested due to automatic review settings May 21, 2026 13:07
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 under src/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.marktextwindow.marktext.
src/renderer/src/store/layout.js Migrates global.marktextwindow.marktext.
src/renderer/src/store/editor.js Migrates global.marktextwindow.marktext.
src/renderer/src/prefComponents/keybindings/index.vue Migrates global.marktextwindow.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.marktextwindow.marktext.
src/renderer/src/pages/app.vue Migrates global.marktextwindow.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.marktextwindow.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.marktextwindow.marktext.
src/renderer/src/commands/index.js Replaces remote window calls and async-gates update command registration.
src/renderer/src/bootstrap.js Migrates global.marktextwindow.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.

Comment thread src/preload/index.js Outdated
Comment thread src/preload/index.js Outdated
Comment thread src/renderer/src/commands/index.js Outdated
Comment thread src/renderer/src/commands/index.js Outdated
Comment thread src/main/ipc/uploader.js Outdated
Comment thread src/main/ipc/window.js
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 21, 2026

Build artifacts for PR #4244:

Run: https://github.com/marktext/marktext/actions/runs/26260974817

Artifact Size Link
marktext-linux 594.5 MB Download
marktext-macos-x64 534.9 MB Download
marktext-macos-arm64 534.6 MB Download
marktext-windows 272.8 MB Download

Jocs and others added 5 commits May 21, 2026 21:52
…-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>
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>
@Jocs Jocs merged commit fa84fc0 into develop May 22, 2026
10 checks passed
@Jocs Jocs deleted the worktree-expressive-soaring-swan branch May 22, 2026 00:42
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants