Template tests / tests (pull_request) Failing after 33s
Phase 1 of the improvement plan (PR 2 of the sequence). A remote page could previously inherit the privileged preload bridge: the main window had no navigation guard or popup handler, IPC handlers accepted any sender, every Electron permission was granted to everyone, and shell:openPath accepted an arbitrary renderer-supplied target. - New app/security.js (plain-Node testable) centralizes the policy: app-page identity, navigation/popup denial, deny-by-default permissions, external-URL validation, IPC sender guard, payload budgets, field validators, and a produced-files registry. - Every window (main, region overlay, capture worker) is sandboxed, denies all navigation away from its own page, denies every popup, and refuses webview attachment. - Every IPC channel now rejects events that are not from the main window's top frame on index.html, rejects non-plain/oversized argument bags, and channels with risky inputs validate ids, enums, names, and sizes (path-traversal and prototype-pollution guards included). - Permissions are deny-by-default; the only grant in the app is display capture (+media) for the dedicated capture-worker page. The display-media handler also verifies the requesting frame. - shell:openPath/showItemInFolder are gone. Replacements are intent-specific: openProduced (only files the main process produced this session), revealLinkedArchive (path read from the store, not the renderer), and openExternal (parsed, scheme-checked http(s)/mailto only). - archive:peek removed: unused, and it let the renderer read arbitrary archives by path. - export:run only accepts output directories that came from a dialog pick or remembered settings; anything else re-prompts. - Renderer: description links never navigate; http(s)/mailto open externally via the validated handler. - 12 new security regression tests: hostile navigation targets, permission matrix, sender spoofing (wrong window/subframe/navigated/disposed frames), oversized payloads, traversal/pollution attempts, produced-file registry, and source-level guards (no blanket grants, no generic shell channels, sandbox on every window). Verified: 215 unit tests pass; startup smoke, unit-workflows, sample artifacts, and build-release E2E pass; click self-test still reaches source: stream with markers 3/3 and burst 8/8 under the deny-by-default policy (arm/debounce remain the known pre-existing failures, untouched here); UI screenshot confirms the sandboxed renderer boots. Co-Authored-By: Claude Fable 5 <[email protected]>
110 lines
3.4 KiB
JavaScript
110 lines
3.4 KiB
JavaScript
'use strict';
|
|
|
|
const { contextBridge, ipcRenderer } = require('electron');
|
|
|
|
/**
|
|
* The complete privileged API exposed to the sandboxed renderer. Every call
|
|
* is an explicit invoke; no raw ipcRenderer or Node access leaks through.
|
|
*/
|
|
|
|
const invoke = (channel) => (args) => ipcRenderer.invoke(channel, args);
|
|
|
|
const api = {
|
|
library: {
|
|
list: invoke('library:list'),
|
|
create: invoke('library:create'),
|
|
duplicate: invoke('library:duplicate'),
|
|
delete: invoke('library:delete'),
|
|
setFavorite: invoke('library:setFavorite'),
|
|
trashList: invoke('library:trash:list'),
|
|
trashRestore: invoke('library:trash:restore'),
|
|
trashPurge: invoke('library:trash:purge'),
|
|
},
|
|
folders: {
|
|
create: invoke('folders:create'),
|
|
rename: invoke('folders:rename'),
|
|
delete: invoke('folders:delete'),
|
|
moveGuide: invoke('folders:moveGuide'),
|
|
},
|
|
guide: {
|
|
get: invoke('guide:get'),
|
|
save: invoke('guide:save'),
|
|
},
|
|
step: {
|
|
add: invoke('step:add'),
|
|
save: invoke('step:save'),
|
|
delete: invoke('step:delete'),
|
|
restore: invoke('step:restore'),
|
|
reorder: invoke('steps:reorder'),
|
|
imagePath: invoke('step:imagePath'),
|
|
setWorkingImage: invoke('step:setWorkingImage'),
|
|
resetWorkingImage: invoke('step:resetWorkingImage'),
|
|
fromClipboard: invoke('step:fromClipboard'),
|
|
importImage: invoke('step:importImage'),
|
|
},
|
|
search: {
|
|
query: invoke('search:query'),
|
|
titles: invoke('search:titles'),
|
|
},
|
|
settings: {
|
|
all: invoke('settings:all'),
|
|
set: invoke('settings:set'),
|
|
globalPlaceholders: invoke('placeholders:globals:get'),
|
|
setGlobalPlaceholders: invoke('placeholders:globals:set'),
|
|
},
|
|
ai: {
|
|
test: invoke('ai:test'),
|
|
fillStep: invoke('ai:fillStep'),
|
|
rewriteText: invoke('ai:rewriteText'),
|
|
},
|
|
capture: {
|
|
shoot: invoke('capture:shoot'),
|
|
region: invoke('capture:region'),
|
|
session: invoke('capture:session'),
|
|
state: invoke('capture:state'),
|
|
onAdded: (fn) => ipcRenderer.on('capture:added', (e, payload) => fn(payload)),
|
|
onState: (fn) => ipcRenderer.on('capture:state', (e, payload) => fn(payload)),
|
|
onStepUpdated: (fn) => ipcRenderer.on('step:updated', (e, payload) => fn(payload)),
|
|
},
|
|
archive: {
|
|
export: invoke('archive:export'),
|
|
open: invoke('archive:open'),
|
|
saveLinked: invoke('archive:saveLinked'),
|
|
},
|
|
snapshots: {
|
|
list: invoke('snapshots:list'),
|
|
create: invoke('snapshots:create'),
|
|
restore: invoke('snapshots:restore'),
|
|
},
|
|
templates: {
|
|
list: invoke('templates:list'),
|
|
load: invoke('templates:load'),
|
|
save: invoke('templates:save'),
|
|
delete: invoke('templates:delete'),
|
|
rename: invoke('templates:rename'),
|
|
duplicate: invoke('templates:duplicate'),
|
|
export: invoke('templates:export'),
|
|
import: invoke('templates:import'),
|
|
},
|
|
export: {
|
|
formats: invoke('export:formats'),
|
|
defaults: invoke('export:defaults'),
|
|
run: invoke('export:run'),
|
|
chooseDir: invoke('export:chooseDir'),
|
|
preview: invoke('export:preview'),
|
|
cleanupPreviews: invoke('preview:cleanup'),
|
|
},
|
|
shell: {
|
|
// Intent-specific shell access only: files the main process produced,
|
|
// the guide's linked archive, and scheme-validated external links.
|
|
openProduced: invoke('shell:openProduced'),
|
|
revealLinkedArchive: invoke('shell:revealLinkedArchive'),
|
|
openExternal: invoke('shell:openExternal'),
|
|
},
|
|
app: {
|
|
info: invoke('app:info'),
|
|
},
|
|
};
|
|
|
|
contextBridge.exposeInMainWorld('stepforge', api);
|