mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-17 16:37:43 +00:00
* docs(plugins): add microsoft 365 calendar provider plan * docs(plugins): add microsoft 365 calendar provider plan * docs(ios): plan internal testflight builds * fix(electron): assert renderer IPC boundary at window creation Every IPC trust boundary (Jira one-shot capability, plugin node-exec consent, the window.ea preload bridge) rests on the renderer main world not having require/ipcRenderer, which is guaranteed solely by contextIsolation: true + nodeIntegration: false and sub-frames not getting node integration. If that webPreferences ever silently regressed (a refactor spreading a shared options object, a bad merge), every gate would collapse at once while still looking correct in review. Add web-preferences-guard.ts (assertSecureWebPreferences) and fail closed before creating a window if the boundary is not intact. It rejects a non-true contextIsolation, a non-false nodeIntegration, and (fail-closed) a nodeIntegrationInSubFrames that is not explicitly false; it also directionally rejects an explicit sandbox: false, nodeIntegrationInWorker: true, and webviewTag: true (each off by default, so no call site is forced to set it). Wire it at all three new BrowserWindow sites (main window, task widget, full-screen blocker); the full-screen blocker previously relied on Electron defaults, so set its webPreferences explicitly. A *.test.cjs backs it with behavioral coverage plus a wiring guard that counts constructor sites vs guard calls per file, so a future window cannot silently ship without the check. Closes #9015 * fix(electron): extend webPreferences guard to webSecurity Follow-up hardening from the multi-agent review of #9018: - Reject an explicit `webSecurity: false` (directional, like the sandbox /worker/webviewTag trio). With the app's blanket Access-Control-Allow-Origin: *, disabling the same-origin policy in a node-bridged renderer would widen cross-origin reach — and no call site currently guards against it. - Broaden the wiring-guard test to also require the assert for `new BrowserView` / `new WebContentsView`, closing the tripwire's blind spot for future non-BrowserWindow renderers (none exist today). - Correct the fail() comment: the `throw` narrows the type regardless of return-vs-throw; fail() returns an Error only to DRY the message. 230/230 electron tests pass; checkFile + prettier clean.
96 lines
4.7 KiB
TypeScript
96 lines
4.7 KiB
TypeScript
import { BrowserWindowConstructorOptions } from 'electron';
|
|
|
|
type WebPreferences = BrowserWindowConstructorOptions['webPreferences'];
|
|
|
|
/**
|
|
* Fail-closed guard for a renderer's security-critical webPreferences.
|
|
*
|
|
* Every IPC trust boundary in the app — the Jira one-shot capability, plugin
|
|
* node-execution consent, the `window.ea` preload bridge — ultimately rests on
|
|
* the renderer main world NOT having `require` / `ipcRenderer`. That property is
|
|
* guaranteed solely by `contextIsolation: true` + `nodeIntegration: false`, plus
|
|
* sub-frames (where untrusted plugin iframes run) not getting node integration.
|
|
* If any of those silently regressed — a refactor spreading a shared options
|
|
* object, a bad merge, a copy-paste into a new window — every one of those gates
|
|
* would collapse at once while still looking correct in a diff.
|
|
*
|
|
* This asserts the invariant at window creation and throws BEFORE the window
|
|
* loads, so an accidental regression fails the app at startup / in CI instead of
|
|
* shipping a renderer that plugin code can fully own. It is a tripwire against
|
|
* accidental drift, not a defense against a developer who deliberately flips a
|
|
* flag (they would delete this call too).
|
|
*
|
|
* Two kinds of check:
|
|
* - The three core boundary flags — `contextIsolation`, `nodeIntegration`,
|
|
* `nodeIntegrationInSubFrames` — are **fail-closed**: an omitted/`undefined`
|
|
* value is rejected too, so the guard never depends on the Electron default
|
|
* staying safe across upgrades. (Sub-frames are included because that flag
|
|
* governs whether the preload bridge reaches plugin iframes.)
|
|
* - The additional insecure overrides — `sandbox`, `nodeIntegrationInWorker`,
|
|
* `webviewTag`, `webSecurity` — are checked **directionally**: only an explicit
|
|
* insecure value is rejected; an omitted key keeps Electron's secure default so
|
|
* no call site is forced to enumerate them. These stay default-dependent by choice.
|
|
*
|
|
* Scope notes:
|
|
* - Electron exposes no getter for a webContents' *effective* webPreferences, so
|
|
* this can only validate the options object we pass to the constructor.
|
|
* - The wiring-guard test requires this call for `new BrowserWindow`,
|
|
* `new BrowserView`, and `new WebContentsView`. A `<webview>` guest has no such
|
|
* constructor and would still need its own validation (e.g. a
|
|
* `will-attach-webview` handler) — none of these exist today.
|
|
*/
|
|
export const assertSecureWebPreferences = (
|
|
webPreferences: WebPreferences,
|
|
windowLabel: string,
|
|
): void => {
|
|
// Returns an Error (callers `throw fail(...)`) so the shared message prefix/suffix
|
|
// is defined once — mirroring the `throw fail(...)` shape of the sibling guard
|
|
// `file-path-guard.ts` (that one also hardens the error for the renderer; here the
|
|
// error only ever surfaces in the main process, so it needs no such hardening).
|
|
const fail = (detail: string): Error =>
|
|
new Error(
|
|
`Insecure webPreferences for the "${windowLabel}" window: ${detail}. ` +
|
|
'This would collapse the renderer IPC trust boundary — refusing to create the window.',
|
|
);
|
|
|
|
if (!webPreferences) {
|
|
throw fail('no webPreferences set (relying on Electron defaults)');
|
|
}
|
|
// Core boundary flags — fail-closed (reject omitted/undefined too).
|
|
if (webPreferences.contextIsolation !== true) {
|
|
throw fail(
|
|
`contextIsolation must be true (got ${String(webPreferences.contextIsolation)})`,
|
|
);
|
|
}
|
|
if (webPreferences.nodeIntegration !== false) {
|
|
throw fail(
|
|
`nodeIntegration must be false (got ${String(webPreferences.nodeIntegration)})`,
|
|
);
|
|
}
|
|
if (webPreferences.nodeIntegrationInSubFrames !== false) {
|
|
throw fail(
|
|
`nodeIntegrationInSubFrames must be false (got ${String(webPreferences.nodeIntegrationInSubFrames)})`,
|
|
);
|
|
}
|
|
// Additional node-capability surfaces — directional (reject explicit insecure
|
|
// value only). Disabling the sandbox re-enables full Node in the preload; a
|
|
// Node-enabled worker or a <webview> guest would each open a path around the
|
|
// IPC/consent boundary.
|
|
if (webPreferences.sandbox === false) {
|
|
throw fail('sandbox must not be explicitly false');
|
|
}
|
|
if (webPreferences.nodeIntegrationInWorker === true) {
|
|
throw fail('nodeIntegrationInWorker must not be true');
|
|
}
|
|
if (webPreferences.webviewTag === true) {
|
|
throw fail(
|
|
'webviewTag must not be true (a <webview> guest needs its own validation)',
|
|
);
|
|
}
|
|
// webSecurity is the same-origin policy rather than a node capability, but with the
|
|
// app's blanket Access-Control-Allow-Origin: * an explicit `false` here would widen
|
|
// a node-bridged renderer's cross-origin reach — reject it (directional, like above).
|
|
if (webPreferences.webSecurity === false) {
|
|
throw fail('webSecurity must not be explicitly false');
|
|
}
|
|
};
|