Skip to content

RFC: Architectural Upgrades for Security (Context Isolation & Auto-Update Integrity) and CPU Idle Efficiency #3377

Description

@Shevilll

RFC: Hardening Rocket.Chat.Electron's Security Posture and Enhancing CPU/Resource Efficiency

Status: Proposed

Author: Shevilll
Target: RocketChat/Rocket.Chat.Electron
Focus Areas: Security (Context Isolation & Secure Preloads), Supply Chain Security (Auto-Updater Verification), and Resource Optimization (CPU Idle States).


1. Executive Summary

As Rocket.Chat Desktop continues to serve millions of users globally across macOS, Windows, and Linux, modernizing its core architecture is critical to keeping pace with security standards and energy efficiency expectations.

This RFC proposes three critical, interconnected architectural upgrades to Rocket.Chat.Electron:

  1. Secure-by-Default Window Isolation: Moving from legacy configurations with contextIsolation: false and nodeIntegration: true to a modern, zero-trust context-isolated environment via contextBridge across all BrowserWindow targets (rootWindow, logViewerWindow, videoCallWindow).
  2. Cryptographic Update Integrity: Introducing explicit post-download signature and SHA256 checksum validations to safeguard the auto-update mechanism against man-in-the-middle (MitM) and supply-chain tampering.
  3. Event-Driven & Power-Aware Resource Tuning: Replacing wasteful 1-second interval DOM-polling with surgical MutationObserver subscriptions and leveraging Electron's powerMonitor API to put background tabs, rendering cycles, and event loops to sleep when idle.

2. Secure-by-Default Isolation (Context Isolation & Preloads)

2.1. The Threat Model & Current Limitations

In the current setup, several critical windows—such as rootWindow and logViewerWindow—are configured with:

webPreferences: {
  nodeIntegration: true,
  contextIsolation: false
}

If a Cross-Site Scripting (XSS) vulnerability is discovered within any local renderer or if untrusted markup is rendered (e.g., via HTML-based logs or shared views), an attacker gains full access to Node.js built-ins (require('child_process'), require('fs')). This enables Remote Code Execution (RCE) directly on the user's host machine.

By enforcing contextIsolation: true and nodeIntegration: false, the renderer process is separated from the preload script, protecting the Node.js runtime from exploit injection.

2.2. Architectural Blueprint: The Secure Bridge

We propose a complete refactoring of the window instantiation preferences and IPC routing.

sequenceDiagram
    participant Renderer as Renderer Process (Web Content)
    participant Bridge as Context Bridge (Preload Script)
    participant Main as Main Process (Node.js)
    
    Renderer->>Bridge: Calls filtered API: RocketChatDesktop.send('log-action', data)
    Note over Bridge: Validates channel is allowed<br/>and sanitizes payload
    Bridge->>Main: ipcRenderer.send('log-action', sanitizedData)
    Note over Main: Executes Node.js logic safely
Loading

Refactored Window Instantiation (src/ui/main/rootWindow.ts)

const webPreferences: WebPreferences = {
  nodeIntegration: false,
  nodeIntegrationInSubFrames: false,
  contextIsolation: true,
  sandbox: true, // Enable chromium-level process sandboxing
  preload: path.join(app.getAppPath(), 'app/preload.js'),
  webviewTag: false, // Transition webviews to iframe sandboxes where possible
};

Concrete Preload Implementation (src/preload.ts)

import { contextBridge, ipcRenderer } from 'electron';

// Explicitly define and freeze the allowed IPC channels
const ALLOWED_SEND_CHANNELS = new Set([
  'server-touch',
  'min-window',
  'max-window',
  'close-window',
  'open-external-url',
]);

const ALLOWED_RECEIVE_CHANNELS = new Set([
  'server-selected',
  'update-downloaded',
  'power-state-change',
]);

contextBridge.revealInMainWorld('RocketChatDesktop', {
  send: (channel: string, data: any) => {
    if (ALLOWED_SEND_CHANNELS.has(channel)) {
      // Strip prototype pollution and ensure deep cloning of data
      const safeData = JSON.parse(JSON.stringify(data));
      ipcRenderer.send(channel, safeData);
    } else {
      console.warn(`Blocked unauthorized outgoing IPC channel: ${channel}`);
    }
  },
  on: (channel: string, callback: (...args: any[]) => void) => {
    if (ALLOWED_RECEIVE_CHANNELS.has(channel)) {
      const subscription = (_event: any, ...args: any[]) => callback(...args);
      ipcRenderer.on(channel, subscription);
      return () => {
        ipcRenderer.removeListener(channel, subscription);
      };
    } else {
      console.warn(`Blocked unauthorized incoming IPC channel: ${channel}`);
      return () => {};
    }
  }
});

3. Auto-Updater Cryptographic Integrity Checks

3.1. Mitigating Update Spoofing & Supply Chain Attacks

While electron-updater performs generic certificate verification on macOS and Windows, relying solely on package certificates leaves gaps if certificate authorities are compromised or if local configurations are manipulated.

We propose a multi-layered verification strategy that intersects with the auto-update lifecycle. Prior to allowing autoUpdater.quitAndInstall(), the app should perform a rigorous, secondary verify operation on the downloaded binary.

3.2. Implementation Blueprint: Cryptographic Verification Hook

We can hook into the update pipeline to perform an out-of-band SHA256 checksum audit and binary signature verification against a known, signed metadata manifest.

import { autoUpdater } from 'electron-updater';
import { promises as fs } from 'fs';
import * as crypto from 'crypto';
import { execFile } from 'child_process';

autoUpdater.on('update-downloaded', async (info) => {
  const downloadedFile = info.downloadedFile;
  if (!downloadedFile) {
    console.error('Update downloaded but file path is missing.');
    return;
  }

  try {
    // 1. Perform SHA256 Checksum Verification
    const fileBuffer = await fs.readFile(downloadedFile);
    const hash = crypto.createHash('sha256').update(fileBuffer).digest('hex');
    
    // Fetch target hash from a signed, secure manifest hosted on a separate secure endpoint
    const expectedHash = await fetchExpectedHashFromSignedManifest(info.version);
    
    if (hash !== expectedHash) {
      throw new Error(`Checksum mismatch! Expected ${expectedHash}, got ${hash}`);
    }

    // 2. Perform Native Platform Signature Check
    await verifyNativeSignature(downloadedFile);

    console.log('Update package verified successfully. Safe to install.');
    // Notify renderer that update is verified and ready
    rootWindow.webContents.send('update-downloaded', { verified: true, version: info.version });

  } catch (error) {
    console.error('CRITICAL: Update package validation failed!', error);
    // Secure deletion of the invalid binary to prevent execution
    await fs.unlink(downloadedFile).catch(() => {});
  }
});

// Platform-specific signature checks
function verifyNativeSignature(filePath: string): Promise<void> {
  return new Promise((resolve, reject) => {
    if (process.platform === 'win32') {
      // Use Windows PowerShell to verify digital signature
      execFile('powershell.exe', [
        '-NoProfile',
        '-NonInteractive',
        '-Command',
        `Get-AuthenticodeSignature "${filePath}" | Where-Object { $_.Status -eq "Valid" }`
      ], (err, stdout) => {
        if (err || !stdout.trim()) {
          reject(new Error('Windows code signature validation failed.'));
        } else {
          resolve();
        }
      });
    } else if (process.platform === 'darwin') {
      // Use macOS codesign utility to verify developer ID signature
      execFile('codesign', ['-v', '--verbose=4', filePath], (err) => {
        if (err) {
          reject(new Error('macOS codesign verification failed.'));
        } else {
          resolve();
        }
      });
    } else {
      // For Linux, verify package hash signatures if built with GPG signatures
      resolve();
    }
  });
}

4. CPU Idle Optimizations and Energy Efficiency

4.1. The Cost of Polling Loops

Currently, src/injected.ts (which is executed in the context of the main Rocket.Chat application page) relies on an active polling interval to setup reactive features:

setupReactiveFeatures();
setInterval(setupReactiveFeatures, 1000); // Check every second for newly loaded modules

Running a timer every 1000ms indefinitely—regardless of window focus, user activity, or system power state—is extremely inefficient. It forces Chromium's renderer process to wake up constantly, preventing the host CPU from entering low-power idle states. This degrades battery life on laptops.

4.2. Event-Driven Architecture with MutationObserver

Instead of polling, we should subscribe directly to structural modifications in the DOM or the React/Meteor root nodes using MutationObserver.

// Replacement for the active polling loop in src/injected.ts
const registerObserver = () => {
  const targetNode = document.body;
  
  const config = { 
    childList: true, 
    subtree: true 
  };

  const callback = (mutationsList: MutationRecord[]) => {
    let moduleAdded = false;
    for (const mutation of mutationsList) {
      if (mutation.type === 'childList') {
        // Check if Meteor-related or target React/Tracker modules were added
        const hasReactRoots = Array.from(mutation.addedNodes).some(
          (node) => node instanceof Element && node.querySelector('[data-reactroot], #react-root')
        );
        if (hasReactRoots) {
          moduleAdded = true;
          break;
        }
      }
    }

    if (moduleAdded) {
      setupReactiveFeatures();
    }
  };

  const observer = new MutationObserver(callback);
  observer.observe(targetNode, config);
  
  // Return disconnect handle for cleanup
  return () => observer.disconnect();
};

4.3. System Power State Aware Throttling

We can leverage Electron's powerMonitor API in the Main process to actively throttle background windows when the system enters an idle state or when the screen is locked/suspended.

import { powerMonitor, powerSaveBlocker } from 'electron';

// Track active background processes and throttle
powerMonitor.on('suspend', () => {
  console.log('System is suspending. Throttling all background renderers.');
  throttleAllWindows(true);
});

powerMonitor.on('resume', () => {
  console.log('System resumed. Restoring normal performance profile.');
  throttleAllWindows(false);
});

powerMonitor.on('lock-screen', () => {
  console.log('Screen locked. Pausing non-essential tasks.');
  throttleAllWindows(true);
});

powerMonitor.on('unlock-screen', () => {
  throttleAllWindows(false);
});

function throttleAllWindows(throttle: boolean) {
  const windows = BrowserWindow.getAllWindows();
  for (const win of windows) {
    if (win.isDestroyed()) continue;
    
    if (throttle) {
      // Throttle rendering and background intervals completely
      win.webContents.setFrameRate(10); // Throttle rendering to 10 FPS
      win.webContents.send('power-state-change', 'idle');
    } else {
      win.webContents.setFrameRate(60); // Restore normal 60 FPS rendering
      win.webContents.send('power-state-change', 'active');
    }
  }
}

5. Implementation Roadmap

We propose rolling out these updates in incremental, low-risk phases:

┌────────────────────────────────┐
│   Phase 1: Security Foundation │ -> Context bridge configuration, nodeIntegration disabled
└───────────────┬────────────────┘
                ▼
┌────────────────────────────────┐
│   Phase 2: Refactoring IPC     │ -> Channel verification, payload sanitization
└───────────────┬────────────────┘
                ▼
┌────────────────────────────────┐
│   Phase 3: Integrity Logic     │ -> Custom hash & certificate validations
└───────────────┬────────────────┘
                ▼
┌────────────────────────────────┐
│   Phase 4: Power-Aware Tuning  │ -> MutationObservers and PowerMonitor integration
└────────────────────────────────┘

6. Feedback & Discussion

We welcome comments and feedback from core maintainers on:

  • Migrating the custom webview configurations to safe iframe constructs.
  • Expanding signature validations to cover additional Linux package formats (RPM, AppImage).
  • Ideal thresholds for CPU throttling parameters on low-end systems.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions