This document describes what is actually implemented today, honestly. It does not claim protections that aren't there. Status labels used below:
- Implemented — in place and exercised by the current prototype.
- Partial — something exists but has known gaps.
- Placeholder — a folder/doc exists for it; no code yet.
- Missing — not addressed at all; Electron/Chromium defaults apply.
There are three distinct trust levels in this app:
- Main process — fully trusted, full Node/OS access. Only code under
src/main/,src/core/,src/engines/,src/shared/runs here. - Toolbar renderer (
src/renderer/) — our own code, but still runs as a sandboxed Chromium renderer with no Node access, on the assumption that renderer processes are the most exposed part of any Electron app (defense in depth, not because we distrust our own UI code). It can reach the main process only throughwindow.openBrowser(src/main/security/preload.ts). - Content view (whatever site the user navigated to, via
src/engines/chromium/ChromiumEngine.ts) — fully untrusted. It runs with the same hardenedwebPreferencesas the toolbar, but has no preload script at all, so it has zero bridge to the main process. The only way it affects the app is through Electron's ownwebContentsevents (did-navigate,did-fail-load, etc.), observed from the main process.
Defined once in src/shared/constants/secureWebPreferences.ts and applied
to both the toolbar window and the Chromium content view:
contextIsolation: true— the page's JS and Electron's injected APIs run in separate JS contexts; a page cannot reachcontextBridge-exposed objects' internals or prototype-pollute them.nodeIntegration: false— norequire, no Node globals, in any renderer-facing webContents.sandbox: true— the OS-level Chromium sandbox is active for every renderer-facing webContents, including the content view showing arbitrary websites.
Auditing note: since this is one shared constant, confirming these three
settings are correct everywhere means checking one file, not grepping for
duplicated webPreferences object literals.
All renderer↔main communication goes through thirteen named channels in
src/shared/constants/ipcChannels.ts: navigation and tab commands, state
updates, and fixed commands to show the native history, profile, and workspace menus. There is no
generic IPC passthrough and no dynamic channel names.
src/main/security/preload.tsexposes only fixed typed operations viacontextBridge.exposeInMainWorld. It does not exposeipcRendereritself, Node APIs, or anything else.src/main/security/ipcHandlers.tsis the only placeipcMain.onis called. Every handler resolves the acting window via Electron'sevent.sender.id, looked up against controllers registered bysrc/core/browser/BrowserWindowController.ts— a window can only ever control itself, even if a compromised renderer sent a forged payload.- Every command also passes
trustedToolbarSender.ts: the sender must be alive, the sending frame must be that webContents' main frame, and its URL must exactly match the toolbar file URL. Child frames, other local pages, remote pages, and missing frames are rejected. The expected URL and the loaded toolbar path sharesrc/main/windows/toolbarLocation.ts, including when the app is packaged in an asar archive. - The
navigatehandler validatestypeof input === 'string'before doing anything with it. It is otherwise unbounded free text (no length limit) — this is intentional, since address-bar input is meant to be free text, but is worth knowing if you're auditing input validation specifically.
Partial: navigation payload validation at the IPC layer is minimal
(type-only); tab IDs must match tab- followed by one to eight digits. The
navigation-target validation described next is still required in addition
to sender validation.
Profile metadata, history, session data, and saved workspace URL groups are written only by the trusted main process
to the app's local userData directory. Each profile has its own HTTP(S)-only
history/session file; URL-embedded usernames and passwords are stripped.
Additional profiles use separate persistent Chromium partitions for cookies
and site storage. Personal retains Electron's original default Chromium
session and browser-data.json path. The files are not encrypted. History has
no time-based expiry; the user can clear it along with the saved tab set and
workspace URL groups for the active profile from the toolbar. Workspace URLs
are HTTP(S)-validated and URL credentials are stripped. Atomic replacement protects
against a partial JSON write. Malformed session data is ignored with a warning;
malformed workspace data is reported and left untouched.
src/core/navigation/navigation.ts (resolveAddressInput /
isNavigableUrl) is the single choke point every navigation goes through
before ChromiumEngine.navigate() is called:
- Only
http:andhttps:URLs are ever loaded directly. - Anything else —
javascript:,file:,data:, malformed input, or plain text — is not loaded; it's sent to the default search engine as a query string instead. A user typingjavascript:alert(1)gets a search for that literal text, never execution. ChromiumEngine.navigate()independently re-checksisNavigableUrl()before callingloadURL, so this isn't relying on a single call site.
This is pure, Electron-free logic (tests/unit/navigation.test.ts covers
it directly without needing an Electron runtime).
No eval, new Function(...), child_process, or unsafe dynamic
require/import exists anywhere in this codebase as of this reorganization.
The renderer only ever assigns textContent, .value, and toggles
classList — it never assigns innerHTML with data that isn't a fixed
literal string, so there's no DOM-XSS sink from NavigationState fields
(e.g. the error message) reaching the page as markup.
The toolbar document (src/renderer/layout/index.html) sets a restrictive
Content Security Policy through a document meta tag. It allows only local
scripts and styles, blocks network connections and embedded objects, and
restricts forms, frames, and base URLs. tests/unit/rendererSecurity.test.ts
checks that the policy remains present and includes its restrictive directives.
This policy applies only to the browser's toolbar UI. It does not set a policy
for websites opened in content views; those remain untrusted and need separate
privacy/security controls. Whether toolbar CSP is a V1 release gate remains
undecided under ND-12 in docs/SPECIFICATION.md.
Each Chromium session handles both permission checks and requests. Eligible camera, microphone, location, and notification requests from the active top-level page show a native Block / Allow once dialog with the requesting origin; Block is the default. Only HTTPS and loopback HTTP origins are eligible. Unsupported permissions, device access, background tabs, child frames, and requests without a reliable origin are denied. There is no persistent allowlist.
Media and location approval applies to that request. An already active camera,
microphone, or location stream/watch can continue after a tab switch. In an
isolated fake-microphone test, a site copied a stream into a popup and its copy
was briefly live after the original page was destroyed, before the popup closed.
A separate test observed the copy ended after the tab closed. To be safe, close all Open Browser
windows when finished. After camera or microphone approval, a tab badge and a
browser-session warning state that access may be in use. The session warning
also appears in native window titles, including popups, and remains until the
application exits. These warnings are conservative reminders, not live capture
status: they can remain after the device stops or fails to start, and native
window titles may not be visible in fullscreen. No exact capture indicator is
implemented. See docs/decisions/ADR-006-conservative-media-warning.md.
Notification access lasts while that tab remains active on the
current page, and is revoked when it navigates, becomes inactive, or closes.
Pending approvals are denied if the page changes or closes. The operating system
may still deny access after the browser approves a request. See
docs/decisions/ADR-005-temporary-permissions.md for the boundaries.
Each Chromium session configures Electron's native download save prompt with a
suggested filename in the user's Downloads directory. The website-provided
filename is reduced to its basename using either path separator. Invalid
Windows characters and reserved names are handled; empty or dot-only names
fall back to download. The user selects the final location.
Download progress/history management is not implemented. See
src/services/downloads/README.md.
No custom certificate-error handler is installed, so Chromium validates
certificates and presents its normal error behavior. This is intentional:
custom overrides could let invalid certificates through. The prototype does
not add a bypass or exception flow.
The CI-built Windows installer (.github/workflows/build-windows.yml) is
unsigned and is not auto-installed or auto-updated anywhere. There is no
in-app update check. Shipping real auto-updates safely requires a separate
signed-update system (code-signing the installer + a trusted update feed,
e.g. via electron-updater) — this does not exist yet. See
docs/ROADMAP.md and the CI section of the root README.md.
Read, in this order: src/main/security/ (both files, they're short),
src/shared/constants/secureWebPreferences.ts,
src/core/navigation/navigation.ts, then
src/core/browser/BrowserWindowController.ts. That's the entire trust
boundary plus the two things that cross it (window creation and
navigation). Everything else in src/core/, src/engines/, and
src/renderer/ operates strictly inside the boundaries those files set.