free-piAll articles

Reverse engineering

Is your Electron app hardened? Reading its fuses and IPC surface

Electron apps are web pages with a desktop shell, and their security depends on a few settings: the Electron "fuses" (switches compiled into the app's binary), the options of each window, what the preload script exposes

Published

Electron apps are web pages with a desktop shell, and their security depends on a few settings: the Electron "fuses" (switches compiled into the app's binary), the options of each window, what the preload script exposes to the page, and which IPC channels (messages between the page and the main process) exist. We asked an AI model, using REA's tools, to read these four things for Mark Text, an open-source Markdown editor. It answered all four. Some of its answers came from REA and some did not, and this post says which.

The target

Mark Text is MIT-licensed (https://github.com/marktext/marktext). We analysed the latest release at the time, v0.22.0, the macOS arm64 build:

  • Archive: marktext-mac-arm64-0.22.0.zip
  • Archive SHA-256: 206ce07d24268d42b0c80276d28b7d36914c3b8238ec64a95868fcee10c050f4 (matches the digest GitHub lists for the release asset)
  • Contents/Resources/app.asar SHA-256: 0dc6901ebd66b2ee6e73f28f649969c3aebade7739b8fde855f47f6cc2e870b0 (177,530,497 bytes)
  • Electron Framework binary SHA-256 (as reported by REA): 46265ddc9a9eb7bc431ee6fdf078813681a0aca444b0f86428ac2193554b4331

We downloaded it ourselves and never ran the app.

Requirements

  • A Mac to unpack the macOS build (the analysis itself only reads files).
  • curl and unzip.
  • Node.js that REA accepts: 22.19 or a later 22.x, 24.11 or a later 24.x, or 26 and later. We used 24.15.0. free-pi itself also needs Node.
  • A GitHub account and free-pi 0.4.5 or newer. No API key, no card. free-pi allows one active CLI session per account.
  • Network access for the model. The model also fetched a small npm package on its own during the session (see "What went wrong").

Commands

mkdir -p r03 && cd r03
curl -L -o marktext-mac-arm64-0.22.0.zip \
  https://github.com/marktext/marktext/releases/download/v0.22.0/marktext-mac-arm64-0.22.0.zip
shasum -a 256 marktext-mac-arm64-0.22.0.zip
mkdir mt && unzip -q marktext-mac-arm64-0.22.0.zip -d mt
npx free-pi-cli

Inside free-pi:

/reverse-engineer-anything
/mcp

When /mcp shows rea connected, send one prompt. Use the absolute path to the .app in your own folder:

Use REA to audit the Electron app at /tmp/rea-demo/r03/mt/MarkText.app. Report (1) the
Electron fuse state, (2) the BrowserWindow webPreferences for nodeIntegration,
contextIsolation and sandbox, (3) which APIs the preload script exposes to the renderer,
and (4) the IPC channels between renderer and main. Read the app code from
Contents/Resources/app.asar and keep to these four questions. Cite the tool output
for each fact.

A recorded free-pi session auditing Mark Text with REA. Sped up 12x.

Sped up 12x with long pauses shortened. The recording starts at the /reverse-engineer-anything step; the session took about four minutes in real time.

What the model found

1. Fuse state (from REA)

The model called REA's inspect_electron_fuses on the .app bundle. REA read the fuse bytes from the Electron Framework binary (one arm64 slice, wire version 1, all nine fuses present) without launching the app. Evidence id ev_f4605a6cd53add549e8c4c532f67f53e8be3898fd1a3abf3579f5e3671e04be6.

  • RunAsNode: enabled
  • EnableCookieEncryption: disabled
  • EnableNodeOptionsEnvironmentVariable: enabled
  • EnableNodeCliInspectArguments: disabled
  • EnableEmbeddedAsarIntegrityValidation: disabled
  • OnlyLoadAppFromAsar: disabled
  • LoadBrowserProcessSpecificV8Snapshot: disabled
  • GrantFileProtocolExtraPrivileges: enabled
  • WasmTrapHandlers: enabled

REA also lists what these imply, and the model passed them on: ELECTRON_RUN_AS_NODE is honored, embedded ASAR integrity is not enforced, and loading is not restricted to app.asar. REA adds a caveat that the model repeated: fuse bytes do not prove the app cannot turn the inspector on some other way in its own code.

2. Window settings (from the source, not from REA)

REA's analysis of the app code (analyze_javascript_application, run on package.json, out/main/index.js and out/preload/index.js inside the ASAR) found 2 browser windows but reported explicit_web_preferences: 0. The model explained why: the options objects are defined separately from the new BrowserWindow(...) calls and merged with Object.assign, so REA's call-site check saw no literal options.

The model therefore extracted the main bundle from the ASAR and read it. This part is not REA output. It reported the same webPreferences for both windows (main bundle lines 3721–3727 and 3740–3746):

webPreferences: {
  contextIsolation: true,
  sandbox: true,
  spellcheck: true,
  nodeIntegration: false,
  webSecurity: false,
  preload: path.default.join(__dirname, "../preload/index.js")
}

So nodeIntegration is off and contextIsolation and sandbox are on, which are the settings you hope to see. The setting that stands out is webSecurity: false, which turns off the browser's same-origin policy in both windows. The model also noted the app sets spellcheck to false at runtime when an env.disableSpellcheck flag is set (line 19939).

3. What the preload exposes (counts from REA, members from the source)

REA's summary counted 11 contextBridge APIs and 50 exposed members. The model read out/preload/index.js (lines 1432–1444 hold the 11 exposeInMainWorld calls) and listed them:

  • electron: an ipcRenderer wrapper (send, sendSync, invoke, on, once, removeAllListeners), shell (open external link, open path, show item), clipboard, fetchLinkHtml, webFrame zoom, webUtils.getPathForFile, a process snapshot (platform, arch, versions, env, working directory), window controls and dialog.showSave
  • process: a small shim (platform, arch, versions, env, cwd(), nextTick)
  • rgPath: a string, the path to the bundled ripgrep binary
  • fileUtils: file operations such as read, write, copy, move, delete, readdir, stat (the model said 18; the preload source has 19 callable members plus one constant, so the model's count was off)
  • path: a subset of Node's path functions
  • commandExists, i18nUtils, ripgrep, uploader, fonts, diagram: one to seven members each

The model's own judgement, which is not a REA result: because the ipcRenderer wrapper passes any channel name and payload through, a cross-site scripting bug in the page would give an attacker a wide reach, and webSecurity: false makes that worse.

4. IPC channels (counts from REA, names from the source)

REA's retained IPC facts gave these counts: 217 IPC operations, 117 literal channels, 114 main-process handlers, 91 renderer transmissions, 12 renderer listeners, 88 renderer/main pairs, 6 dynamic-channel operations, and 0 sender-validation observations.

The model then grepped the extracted main bundle for the names (again, not REA output). It found 46 ipcMain.handle channels, 67 ipcMain.on channels, and 55 distinct webContents.send channels from main to renderer. The names it listed use a mt:: prefix, for example mt::fs::read-file, mt::fs::write-file, mt::shell::open-external, mt::pandoc::command, and mt::rg::start. The 113 literal registrations it counted plus one dynamic registration (ipcMain.on(channel, listener) at line 4335) equal REA's 114. The full list is in the recording and in the session output; we do not repeat all of it here.

The model said REA found 0 sender-validation observations. We did not check the source for sender checks ourselves, so treat that as REA's count only.

What went wrong

  • REA's window summary was wrong for this app. It said 0 explicit webPreferences. The model noticed and checked the source. If you rely on REA's summary alone, you would miss webSecurity: false.
  • The model used a tool outside REA. It ran npx --yes @electron/asar to list and extract files, which downloads an npm package. REA can analyse the ASAR directly, but the model chose to extract the two files it wanted. We did not instruct this and it is a download we did not control. If you want to avoid it, add "do not run other tools or download anything" to the prompt.
  • Command errors. The model's first two extract-file calls both wrote to index.js in the working directory, so the preload overwrote the main bundle. Its retry into separate subdirectories used a relative path to the ASAR and ended with a Node.js error and exit code 1. Absolute paths then worked. The recording shows this.
  • Model narration slips. Its notes at one point guessed that the preload might be bundled inside the main file, because REA reported preload_entrypoints: 0. The source showed a separate preload file. Check the evidence, not the narration.
  • One run. We ran this once, on one build. The answer on your machine can differ.

What it cost

/usage before and after, read inside the recording (the first number is in the part we trimmed from the GIF):

13,902 / 15,000 credits left today
13,617 / 15,000 credits left today

That is 285 credits, from a daily allowance of 15,000. It is more than the earlier posts in this series because the app is large and the model made many tool calls.

Use REA responsibly

Use REA only for lawful, authorized analysis: your own apps, open-source software, CTFs, and security research.

Tool output goes to the model provider, and free-pi stores traces of sessions. Do not analyse files you are not allowed to share. Mark Text is open source, which is why we used it. This post audits settings. It does not show how to attack the app.

We build free-pi, and an AI model produced the analysis shown above.

free-pi is in early testing; features and limits may change.


Series: Use REA (Reverse Engineer Anything) free: no API key, no card, What's really inside this Go CLI?, What does your app store? Reading an app's SQLite database, Inside a .NET tool: metadata, native imports and one method's CIL.