refactor: Revise configuration and IPC architecture documentation for clarity and security enhancements
- Updated CONFIGURATION.md to reflect changes in target app orchestration, environment variables, and production routing. - Enhanced IPC_ARCHITECTURE.md with a focus on privilege separation, standardized operating procedures, and critical audit checklists. - Added detailed guidelines for extending the IPC bridge and maintaining security integrity. - Introduced new package scripts for macOS, Windows, and Linux builds in package.json.
This commit is contained in:
@@ -1,218 +1,243 @@
|
||||
# IPC Architecture & Security Model
|
||||
|
||||
This document explains the security model of the Electron desktop wrapper, documents the existing IPC channels, and provides a step-by-step guide for extending the app with new native features.
|
||||
The Secure Communication Blueprint.
|
||||
|
||||
> This document defines the **Hardened Security Perimeter** and communication topology governing the Desktop Wrapper. Every native capability exposed to the renderer is mediated through a **Non-Bypassable IPC Bridge**, ensuring that the Node.js Main Process remains cryptographically and logically isolated from untrusted web content. Adherence to this document is **mandatory** — deviations constitute security violations subject to immediate remediation.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Security Model](#security-model)
|
||||
- [The Three-Step Bridge Pattern](#the-three-step-bridge-pattern)
|
||||
- [Existing IPC Channels](#existing-ipc-channels)
|
||||
- [Adding a New Feature: Step-by-Step Example](#adding-a-new-feature-step-by-step-example)
|
||||
- [Anti-Patterns to Avoid](#anti-patterns-to-avoid)
|
||||
- [Privilege Separation Model](#privilege-separation-model)
|
||||
- [Standard Operating Procedure: The Three-Step Bridge](#standard-operating-procedure-the-three-step-bridge)
|
||||
- [Verified Channel Manifest](#verified-channel-manifest)
|
||||
- [Extending the Bridge: Guided Walkthrough](#extending-the-bridge-guided-walkthrough)
|
||||
- [Critical Audit Checklist: Anti-Patterns](#critical-audit-checklist-anti-patterns)
|
||||
- [The Gold Standard for Native Integration](#the-gold-standard-for-native-integration)
|
||||
|
||||
---
|
||||
|
||||
## Security Model
|
||||
## Privilege Separation Model
|
||||
|
||||
The Electron desktop wrapper enforces a strict security boundary between the main process (Node.js) and the renderer process (web app). This is critical because the renderer runs untrusted web content that could be compromised by XSS, malicious dependencies, or supply chain attacks.
|
||||
The desktop wrapper enforces a **strict privilege separation** between three execution contexts, each operating under fundamentally different trust levels. This architecture ensures that a compromise in any single layer cannot escalate to full system access.
|
||||
|
||||
### Core Principles
|
||||
### Trust Level Matrix
|
||||
|
||||
| Setting | Value | Why |
|
||||
|---|---|---|
|
||||
| `contextIsolation` | `true` | The preload script runs in an **isolated JavaScript context**. The renderer cannot access Node.js APIs, `require()`, or the preload's scope. |
|
||||
| `nodeIntegration` | `false` | Node.js APIs (`fs`, `child_process`, `os`, etc.) are **completely unavailable** in the renderer. |
|
||||
| `sandbox` | `true` | The renderer process runs in a Chromium sandbox with restricted OS-level access. |
|
||||
| `webSecurity` | `true` | Same-origin policy is enforced. Cross-origin requests follow standard browser rules. |
|
||||
| Context | Trust Level | Privilege Scope | Security Guarantee |
|
||||
|---|---|---|---|
|
||||
| **Main Process** | Fully Trusted | Unrestricted Node.js access: filesystem, network, printers, OS APIs, child processes | Only code authored by the engineering team executes here |
|
||||
| **Preload Script** | Controlled | Restricted to `ipcRenderer.invoke()` and `ipcRenderer.send()` — no direct Node.js access | Executes in a **Hermetically Sealed Context** — isolated from both the Main Process globals and the Renderer's DOM |
|
||||
| **Renderer** | Untrusted | Standard browser sandbox — zero Node.js API surface | Designated as a **Zero-Trust Environment** — may execute third-party code, npm packages, or XSS payloads |
|
||||
|
||||
### What This Means in Practice
|
||||
### Process Topology
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ Main Process │
|
||||
│ Full Node.js access: filesystem, printers, native APIs, │
|
||||
│ auto-updater, child processes, network (unrestricted) │
|
||||
│ MAIN PROCESS [Fully Trusted] │
|
||||
│ │
|
||||
│ ipcMain.handle('channel', handler) │
|
||||
├──────────────────────────────────────────────────────────────────┤
|
||||
│ Preload Script │
|
||||
│ Isolated context. Can use ipcRenderer (send/invoke only). │
|
||||
│ Exposes a MINIMAL API surface via contextBridge. │
|
||||
│ UNRESTRICTED PRIVILEGES │
|
||||
│ Filesystem · Network · Printers · Native APIs · Child Processes │
|
||||
│ Auto-Updater · OS Integration · System Notifications │
|
||||
│ │
|
||||
│ ipcMain.handle('channel', handler) ← Command handlers │
|
||||
│ ipcMain.on('channel', handler) ← Event listeners │
|
||||
│ webContents.send('channel', data) ← Downstream push │
|
||||
├───────────── Non-Bypassable Isolation Boundary ─────────────────┤
|
||||
│ PRELOAD SCRIPT [Secure Gateway] │
|
||||
│ │
|
||||
│ HERMETICALLY SEALED CONTEXT │
|
||||
│ Performs Interface Narrowing: transforms broad IPC capabilities │
|
||||
│ into a minimal, auditable API surface. Acts as the sole │
|
||||
│ authorized mediator between trusted and untrusted contexts. │
|
||||
│ │
|
||||
│ contextBridge.exposeInMainWorld('electronAPI', { ... }) │
|
||||
├──────────────────────────────────────────────────────────────────┤
|
||||
│ Renderer (React App) │
|
||||
│ Standard browser environment. NO Node.js access. │
|
||||
│ Can ONLY call methods on window.electronAPI. │
|
||||
│ Cannot access ipcRenderer, require, fs, etc. │
|
||||
├───────────── Non-Bypassable Isolation Boundary ─────────────────┤
|
||||
│ RENDERER [Zero-Trust Environment] │
|
||||
│ │
|
||||
│ window.electronAPI.someMethod() │
|
||||
│ UNTRUSTED WEB CONTENT │
|
||||
│ Standard browser sandbox. Zero access to: require, __dirname, │
|
||||
│ process, fs, child_process, net, os, ipcRenderer. │
|
||||
│ │
|
||||
│ ONLY authorized interaction vector: │
|
||||
│ window.electronAPI.methodName(args) │
|
||||
└──────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
The renderer communicates with the main process **only** through the API surface defined in the preload script. This API surface is deliberately narrow — each exposed method does exactly one thing.
|
||||
The Preload Script functions as a **Secure Gateway** that performs **Interface Narrowing** — it transforms the broad, unrestricted IPC capabilities of the Main Process into a deliberately narrow, type-safe API surface. The renderer communicates with native functionality **exclusively** through this gateway. There are no alternative paths, no escape hatches, and no backdoors.
|
||||
|
||||
### Enforcement Configuration
|
||||
|
||||
These settings are declared in `BrowserWindow.webPreferences` and are **non-negotiable**:
|
||||
|
||||
| Setting | Value | Enforcement |
|
||||
|---|---|---|
|
||||
| `contextIsolation` | `true` | The Preload executes in a hermetically sealed V8 context. The renderer **cannot** access `require()`, Node.js globals, or any variable from the preload's scope. |
|
||||
| `nodeIntegration` | `false` | **Zero** Node.js API surface in the renderer. `fs`, `child_process`, `os`, `net`, and all built-in modules are completely unavailable. |
|
||||
| `sandbox` | `true` | The renderer process runs inside a **Chromium OS-level sandbox**, restricting system calls and file access at the kernel level. |
|
||||
| `webSecurity` | `true` | The same-origin policy is **strictly enforced**, preventing cross-origin data exfiltration from the renderer. |
|
||||
|
||||
---
|
||||
|
||||
## The Three-Step Bridge Pattern
|
||||
## Standard Operating Procedure: The Three-Step Bridge
|
||||
|
||||
Every native feature follows the same three-step pattern:
|
||||
Every native feature in this architecture **must** follow the Three-Step Bridge — a Standard Operating Procedure (SOP) that ensures traceability, type-safety, and auditability across the entire IPC surface.
|
||||
|
||||
### Step 1: Register the Handler in the Main Process
|
||||
> [!IMPORTANT]
|
||||
> **Deterministic Synchronization**: Maintaining parity between the Main Process handler, the Preload Gateway exposure, and the TypeScript interface declaration is **mandatory**. A mismatch between any two of the three layers will result in either a **Type-Safety Gap** (silent failures in development) or a **Runtime Regression** (crashes in production).
|
||||
|
||||
File: `apps/desktop/src/main/index.ts`
|
||||
### Step 1: Register the Handler — Main Process
|
||||
|
||||
**File:** `apps/desktop/src/main/index.ts`
|
||||
|
||||
```typescript
|
||||
// Use ipcMain.handle for request/response (returns a value)
|
||||
ipcMain.handle('feature:action', async (_event, arg1, arg2) => {
|
||||
// Perform the native operation
|
||||
// COMMAND PATTERN: Use ipcMain.handle for request/response operations
|
||||
// The handler returns a value to the renderer via a resolved Promise.
|
||||
ipcMain.handle('feature:action', async (_event, arg1: string, arg2: number) => {
|
||||
// Validate inputs. Never trust data from the renderer.
|
||||
if (typeof arg1 !== 'string' || typeof arg2 !== 'number') {
|
||||
throw new Error('Invalid arguments');
|
||||
}
|
||||
const result = await someNativeAPI(arg1, arg2);
|
||||
return result;
|
||||
});
|
||||
|
||||
// Use ipcMain.on for fire-and-forget (no return value)
|
||||
ipcMain.on('feature:fire', (_event, data) => {
|
||||
doSomething(data);
|
||||
// EVENT PATTERN: Use ipcMain.on for fire-and-forget operations
|
||||
// No return value — the renderer does not wait for a response.
|
||||
ipcMain.on('feature:fire', (_event, data: SomeType) => {
|
||||
performSideEffect(data);
|
||||
});
|
||||
```
|
||||
|
||||
**Naming convention**: Use `namespace:action` format. Examples: `printer:get-list`, `updater:check`, `fs:read-file`.
|
||||
**Channel naming convention:** `namespace:action` — examples: `printer:get-list`, `updater:check`, `app:get-version`. Namespaces must be unique, descriptive, and never generic.
|
||||
|
||||
### Step 2: Expose via contextBridge in the Preload Script
|
||||
### Step 2: Expose via contextBridge — Preload Gateway
|
||||
|
||||
File: `apps/desktop/src/preload/index.ts`
|
||||
**File:** `apps/desktop/src/preload/index.ts`
|
||||
|
||||
```typescript
|
||||
const electronAPI = {
|
||||
// For request/response channels
|
||||
// Command pattern exposure
|
||||
featureAction: (arg1: string, arg2: number): Promise<ResultType> => {
|
||||
return ipcRenderer.invoke('feature:action', arg1, arg2);
|
||||
},
|
||||
|
||||
// For fire-and-forget channels
|
||||
// Event pattern exposure
|
||||
featureFire: (data: SomeType): void => {
|
||||
ipcRenderer.send('feature:fire', data);
|
||||
},
|
||||
|
||||
// For main→renderer events (push notifications)
|
||||
// Main→Renderer push events (with automatic lifecycle cleanup)
|
||||
onFeatureEvent: createEventSubscription<EventDataType>('feature:event'),
|
||||
};
|
||||
|
||||
contextBridge.exposeInMainWorld('electronAPI', electronAPI);
|
||||
```
|
||||
|
||||
**Rules**:
|
||||
- Never expose `ipcRenderer` directly.
|
||||
- Never expose `ipcRenderer.on` — use the `createEventSubscription()` helper that returns an unsubscribe function.
|
||||
- Always specify TypeScript types for function signatures.
|
||||
**Non-negotiable rules:**
|
||||
- **Never** expose `ipcRenderer` directly — this is a **Catastrophic Failure** pattern.
|
||||
- **Never** expose `ipcRenderer.on` without cleanup — use `createEventSubscription()`, which returns an unsubscribe function for React `useEffect` lifecycle management.
|
||||
- **Always** declare explicit TypeScript types for all function signatures.
|
||||
|
||||
### Step 3: Update TypeScript Declarations in the React App
|
||||
### Step 3: Declare the Interface — React Application
|
||||
|
||||
File: `apps/web/src/types/electron.d.ts`
|
||||
**File:** `apps/web/src/types/electron.d.ts`
|
||||
|
||||
```typescript
|
||||
interface ElectronAPI {
|
||||
// ... existing methods ...
|
||||
|
||||
// New feature
|
||||
featureAction: (arg1: string, arg2: number) => Promise<ResultType>;
|
||||
featureFire: (data: SomeType) => void;
|
||||
onFeatureEvent: (callback: (data: EventDataType) => void) => () => void;
|
||||
}
|
||||
```
|
||||
|
||||
All three files must stay in sync. If you add a channel to the main process, you must expose it in the preload and declare it in the type file.
|
||||
|
||||
---
|
||||
|
||||
## Existing IPC Channels
|
||||
## Verified Channel Manifest
|
||||
|
||||
### Printer Channels
|
||||
The following is the **complete, authoritative registry** of all authorized IPC channels. These channels are the **only** permitted vectors for native interaction. Any IPC channel not listed here is unauthorized and must be treated as a security anomaly.
|
||||
|
||||
| Channel | Direction | Type | Description |
|
||||
|---|---|---|---|
|
||||
| `printer:get-list` | Renderer → Main → Renderer | `invoke` / `handle` | Returns `ElectronPrinterInfo[]` — list of all connected printers. |
|
||||
| `printer:print` | Renderer → Main → Renderer | `invoke` / `handle` | Triggers a print job with given options. Returns `{ success, failureReason? }`. |
|
||||
### Printer Subsystem
|
||||
|
||||
**Main process implementation**: `setupPrinterIPC()` in `src/main/index.ts`
|
||||
| Channel | Direction | Pattern | Payload | Access Control |
|
||||
|---|---|---|---|---|
|
||||
| `printer:get-list` | Renderer → Main → Renderer | `invoke` / `handle` | Returns `ElectronPrinterInfo[]` | Read-only hardware enumeration |
|
||||
| `printer:print` | Renderer → Main → Renderer | `invoke` / `handle` | Accepts `ElectronPrintOptions`, returns `{ success, failureReason? }` | Controlled hardware invocation |
|
||||
|
||||
**Preload exposure**:
|
||||
**Main process handler:** `setupPrinterIPC()` in `src/main/index.ts`
|
||||
|
||||
**Preload Gateway surface:**
|
||||
```typescript
|
||||
getPrinters: () => ipcRenderer.invoke('printer:get-list')
|
||||
print: (options?) => ipcRenderer.invoke('printer:print', options)
|
||||
```
|
||||
|
||||
**React hook**: `useElectronPrinter()` in `apps/web/src/hooks/use-electron-printer.ts`
|
||||
**React consumption hook:** `useElectronPrinter()` in `apps/web/src/hooks/use-electron-printer.ts`
|
||||
|
||||
---
|
||||
|
||||
### Auto-Updater Channels
|
||||
### Auto-Updater Subsystem
|
||||
|
||||
| Channel | Direction | Type | Description |
|
||||
|---|---|---|---|
|
||||
| `updater:check` | Renderer → Main | `invoke` / `handle` | Triggers a manual update check. Returns the check result. |
|
||||
| `updater:install` | Renderer → Main | `send` / `on` | Quits the app and installs the downloaded update. |
|
||||
| `updater:checking` | Main → Renderer | `send` | Emitted when the updater starts checking. |
|
||||
| `updater:available` | Main → Renderer | `send` | Emitted when an update is found. Payload: `UpdateInfo`. |
|
||||
| `updater:not-available` | Main → Renderer | `send` | Emitted when the app is up to date. Payload: `UpdateInfo`. |
|
||||
| `updater:progress` | Main → Renderer | `send` | Emitted during download. Payload: `ProgressInfo`. |
|
||||
| `updater:downloaded` | Main → Renderer | `send` | Emitted when download completes. Payload: `UpdateInfo`. |
|
||||
| `updater:error` | Main → Renderer | `send` | Emitted on error. Payload: error message string. |
|
||||
| Channel | Direction | Pattern | Payload | Access Control |
|
||||
|---|---|---|---|---|
|
||||
| `updater:check` | Renderer → Main | `invoke` / `handle` | Returns update check result | Read-only version query |
|
||||
| `updater:install` | Renderer → Main | `send` / `on` | No payload | Privileged: quits app and installs |
|
||||
| `updater:checking` | Main → Renderer | `send` | No payload | Status notification |
|
||||
| `updater:available` | Main → Renderer | `send` | `UpdateInfo { version, releaseDate, releaseNotes }` | Status notification |
|
||||
| `updater:not-available` | Main → Renderer | `send` | `UpdateInfo` | Status notification |
|
||||
| `updater:progress` | Main → Renderer | `send` | `ProgressInfo { percent, bytesPerSecond, transferred, total }` | Progress telemetry |
|
||||
| `updater:downloaded` | Main → Renderer | `send` | `UpdateInfo` | Status notification |
|
||||
| `updater:error` | Main → Renderer | `send` | Error message string | Error telemetry |
|
||||
|
||||
**Main process implementation**: `setupAutoUpdaterIPC()` and `setupAutoUpdaterEvents()` in `src/main/index.ts`
|
||||
**Main process handlers:** `setupAutoUpdaterIPC()` + `setupAutoUpdaterEvents()` in `src/main/index.ts`
|
||||
|
||||
**Preload exposure**: `checkForUpdates()`, `installUpdate()`, `onUpdateAvailable()`, `onDownloadProgress()`, `onUpdateDownloaded()`, `onUpdateError()`, `onUpdateChecking()`, `onUpdateNotAvailable()`
|
||||
**React consumption hook:** `useElectronUpdater()` in `apps/web/src/hooks/use-electron-updater.ts`
|
||||
|
||||
**React hook**: `useElectronUpdater()` in `apps/web/src/hooks/use-electron-updater.ts`
|
||||
> [!NOTE]
|
||||
> The `updater:install` channel is the **highest-privilege IPC operation** in the system — it terminates the running process and launches a new binary. It should only be triggered by an explicit user action, never automatically.
|
||||
|
||||
---
|
||||
|
||||
## Adding a New Feature: Step-by-Step Example
|
||||
## Extending the Bridge: Guided Walkthrough
|
||||
|
||||
**Scenario**: Add a method to read the app's version from the main process.
|
||||
**Scenario:** Expose the application version to the React UI.
|
||||
|
||||
### 1. Main Process
|
||||
|
||||
In `src/main/index.ts`, add inside `app.whenReady()`:
|
||||
### 1. Main Process — Register Handler
|
||||
|
||||
```typescript
|
||||
// In app.whenReady() callback, src/main/index.ts
|
||||
ipcMain.handle('app:get-version', () => {
|
||||
return app.getVersion();
|
||||
});
|
||||
```
|
||||
|
||||
### 2. Preload Script
|
||||
|
||||
In `src/preload/index.ts`, add to the `electronAPI` object:
|
||||
### 2. Preload Gateway — Expose Method
|
||||
|
||||
```typescript
|
||||
// Add to the electronAPI object, src/preload/index.ts
|
||||
const electronAPI = {
|
||||
// ... existing methods ...
|
||||
|
||||
getAppVersion: (): Promise<string> => {
|
||||
return ipcRenderer.invoke('app:get-version');
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### 3. TypeScript Declarations
|
||||
|
||||
In `apps/web/src/types/electron.d.ts`, add to the `ElectronAPI` interface:
|
||||
### 3. TypeScript Interface — Declare Type
|
||||
|
||||
```typescript
|
||||
// Add to ElectronAPI interface, apps/web/src/types/electron.d.ts
|
||||
interface ElectronAPI {
|
||||
// ... existing methods ...
|
||||
|
||||
getAppVersion: () => Promise<string>;
|
||||
}
|
||||
```
|
||||
|
||||
### 4. React Usage
|
||||
### 4. React — Consume
|
||||
|
||||
```tsx
|
||||
function VersionDisplay() {
|
||||
function VersionBadge() {
|
||||
const [version, setVersion] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
@@ -222,69 +247,174 @@ function VersionDisplay() {
|
||||
}, []);
|
||||
|
||||
if (!version) return null;
|
||||
return <span>v{version}</span>;
|
||||
return <span className="version-badge">v{version}</span>;
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Update This Manifest
|
||||
|
||||
After implementing a new channel, **add it to the Verified Channel Manifest** in this document. Undocumented channels are unauthorized channels.
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
## Critical Audit Checklist: Anti-Patterns
|
||||
|
||||
### ❌ Never expose `ipcRenderer` directly
|
||||
The following patterns constitute **critical security violations**. Each one expands the attack surface from "browser-level sandboxed web content" to "unrestricted OS-level code execution." Their presence in production code warrants **immediate incident response**.
|
||||
|
||||
---
|
||||
|
||||
### ❌ Exposing raw `ipcRenderer`
|
||||
|
||||
```typescript
|
||||
// BAD — gives the renderer unrestricted IPC access
|
||||
// VIOLATION: Catastrophic Failure — Total Attack Surface Expansion
|
||||
contextBridge.exposeInMainWorld('ipc', ipcRenderer);
|
||||
```
|
||||
|
||||
### ❌ Never expose `require` or Node.js APIs
|
||||
**Threat:** The renderer gains **unrestricted IPC access** — it can invoke any channel, including channels that were never intended to be callable from the renderer. A single XSS vulnerability escalates to arbitrary native code execution.
|
||||
|
||||
**Classification:** **Total System Compromise**
|
||||
|
||||
---
|
||||
|
||||
### ❌ Exposing `require` or Node.js APIs
|
||||
|
||||
```typescript
|
||||
// BAD — allows arbitrary code execution from the renderer
|
||||
// VIOLATION: Unauthenticated Code Execution
|
||||
contextBridge.exposeInMainWorld('require', require);
|
||||
```
|
||||
|
||||
### ❌ Never use `nodeIntegration: true`
|
||||
**Threat:** The renderer can `require('child_process').exec('rm -rf /')`. A single XSS vulnerability in *any* dependency — including transitive ones — escalates to **full filesystem access, credential theft, reverse shells, and data exfiltration**.
|
||||
|
||||
**Classification:** **Total System Compromise**
|
||||
|
||||
---
|
||||
|
||||
### ❌ Enabling `nodeIntegration`
|
||||
|
||||
```typescript
|
||||
// BAD — completely disables the security boundary
|
||||
// VIOLATION: Catastrophic Failure — Complete Boundary Collapse
|
||||
new BrowserWindow({
|
||||
webPreferences: { nodeIntegration: true, contextIsolation: false }
|
||||
});
|
||||
```
|
||||
|
||||
### ❌ Never pass unsanitized IPC data to shell commands
|
||||
**Threat:** Every `<script>` tag in the renderer — including XSS payloads, compromised npm packages, and injected analytics scripts — gains full Node.js capabilities. The isolation boundary **ceases to exist**.
|
||||
|
||||
**Classification:** **Total System Compromise**
|
||||
|
||||
---
|
||||
|
||||
### ❌ Passing unsanitized IPC data to shell commands
|
||||
|
||||
```typescript
|
||||
// BAD — command injection vulnerability
|
||||
// VIOLATION: Command Injection — Unauthenticated Code Execution
|
||||
ipcMain.handle('run-cmd', (_event, cmd: string) => {
|
||||
exec(cmd); // Attacker can run ANY command
|
||||
exec(cmd); // The renderer controls the command string
|
||||
});
|
||||
```
|
||||
|
||||
### ✅ Always validate IPC arguments in the main process
|
||||
**Threat:** The renderer can execute **arbitrary system commands** with the privileges of the Electron main process (typically the current user). This is the most direct path from XSS to OS-level compromise.
|
||||
|
||||
**Classification:** **Unauthenticated Code Execution**
|
||||
|
||||
---
|
||||
|
||||
### ❌ Registering overly broad IPC channels
|
||||
|
||||
```typescript
|
||||
// GOOD — validate and constrain inputs
|
||||
// VIOLATION: Attack Surface Expansion — Unrestricted File Read
|
||||
ipcMain.handle('file:read', (_event, path: string) => {
|
||||
return readFileSync(path, 'utf-8'); // No validation
|
||||
});
|
||||
```
|
||||
|
||||
**Threat:** The renderer can read **any file** on the filesystem — SSH keys, environment files, database credentials, browser cookies. Input validation is not optional.
|
||||
|
||||
**Classification:** **Sensitive Data Exfiltration**
|
||||
|
||||
---
|
||||
|
||||
## The Gold Standard for Native Integration
|
||||
|
||||
The following patterns represent the **mandatory standard** for all IPC implementations. Adherence is non-negotiable.
|
||||
|
||||
### ✅ Validate and constrain all IPC arguments
|
||||
|
||||
```typescript
|
||||
// GOLD STANDARD: Input validation, path confinement, scope restriction
|
||||
ipcMain.handle('file:read', async (_event, filename: string) => {
|
||||
// Validate: only allow specific filenames, no path separators
|
||||
if (filename.includes('/') || filename.includes('\\')) {
|
||||
// Reject path separators — confine to a single directory
|
||||
if (filename.includes('/') || filename.includes('\\') || filename.includes('..')) {
|
||||
throw new Error('Invalid filename');
|
||||
}
|
||||
|
||||
// Resolve within a controlled directory only
|
||||
const safePath = join(app.getPath('userData'), 'data', filename);
|
||||
|
||||
// Verify the resolved path stays within bounds
|
||||
if (!safePath.startsWith(join(app.getPath('userData'), 'data'))) {
|
||||
throw new Error('Path traversal detected');
|
||||
}
|
||||
|
||||
return readFileSync(safePath, 'utf-8');
|
||||
});
|
||||
```
|
||||
|
||||
### ✅ Always return unsubscribe functions for event listeners
|
||||
**Principle:** Never trust data originating from the renderer. Validate types, constrain scope, and verify resolved paths.
|
||||
|
||||
---
|
||||
|
||||
### ✅ Return unsubscribe functions — Memory Leak Mitigation
|
||||
|
||||
```typescript
|
||||
// GOOD — prevents memory leaks in React's useEffect
|
||||
onSomeEvent: createEventSubscription<DataType>('channel:event')
|
||||
// GOLD STANDARD: The createEventSubscription helper ensures automatic cleanup
|
||||
function createEventSubscription<T>(channel: string) {
|
||||
return (callback: (data: T) => void): (() => void) => {
|
||||
const handler = (_event: Electron.IpcRendererEvent, data: T) => callback(data);
|
||||
ipcRenderer.on(channel, handler);
|
||||
|
||||
// In React:
|
||||
// Return an unsubscribe function — critical for React lifecycle
|
||||
return () => {
|
||||
ipcRenderer.removeListener(channel, handler);
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
**In React's `useEffect`:**
|
||||
|
||||
```tsx
|
||||
useEffect(() => {
|
||||
const unsub = window.electronAPI.onSomeEvent((data) => { /* ... */ });
|
||||
return () => unsub(); // Cleanup on unmount
|
||||
if (!window.electronAPI) return;
|
||||
|
||||
// Subscribe — handler is registered in the Preload's IPC layer
|
||||
const unsub = window.electronAPI.onSomeEvent((data) => {
|
||||
setState(data);
|
||||
});
|
||||
|
||||
// Cleanup on unmount — prevents listener accumulation
|
||||
return () => unsub();
|
||||
}, []);
|
||||
```
|
||||
|
||||
**Principle:** Without the unsubscribe pattern, every component mount adds a **new IPC listener** that persists after unmount. Over time — especially with React's StrictMode double-mounting in development — this causes **memory leaks**, **duplicate event handling**, and **performance degradation**. The `createEventSubscription` helper enforces automatic, deterministic cleanup tied to React's component lifecycle.
|
||||
|
||||
---
|
||||
|
||||
### ✅ Gate all Electron calls behind runtime detection
|
||||
|
||||
```typescript
|
||||
// GOLD STANDARD: Environment-safe consumption
|
||||
function useElectronFeature() {
|
||||
const isElectron = typeof window !== 'undefined' && !!window.electronAPI;
|
||||
|
||||
const doSomething = useCallback(() => {
|
||||
if (!window.electronAPI) return; // No-op in browser
|
||||
window.electronAPI.someMethod();
|
||||
}, []);
|
||||
|
||||
return { isElectron, doSomething };
|
||||
}
|
||||
```
|
||||
|
||||
**Principle:** The React app must run identically in both Electron and standard browser environments. All `window.electronAPI` access must be gated behind a runtime check. Never assume the IPC bridge exists.
|
||||
|
||||
Reference in New Issue
Block a user