import { randomBytes } from 'crypto'; import { promises as fs } from 'fs'; import * as path from 'path'; import { app } from 'electron'; import { assertPathOutside } from './file-path-guard'; /** * Main-owned cache for user-picked images (e.g. background images). * * Background (issue #8228): the legacy flow was * pick → renderer holds an absolute path / file:// URL → renderer asks * main to inline that path as a data URL on every render. * That gave any compromised renderer the ability to ask main to read any * image-extension file outside userData, indefinitely. The path was the * authorization token, and the path could be swapped after the pick. * * After this module: the renderer picks a file via `dialog.showOpenDialog` * (proven user intent), main copies the file into a private cache directory * under userData and hands back an opaque `id`. The renderer stores that id * in user config and asks main for the data URL by id. No path ever leaves * main. Subsequent app launches can resolve the id without re-asking the * user — the file is now owned by the app. * * Source-path validation is layered defense-in-depth: * - source must live outside userData (no laundering the grant file) * - extension must be in the allow-list (binary blobs masquerading as png * won't decode in the renderer anyway, but reject early) * - size must be under MAX_IMAGE_BYTES (avoid memory pressure when * copying / base64-encoding) * - the id is `randomBytes(16)` (128 bits) — unguessable, so a renderer * cannot iterate the cache directory looking for files it didn't import. */ const MAX_IMAGE_BYTES = 5 * 1024 * 1024; // SVG is deliberately excluded: it is a scriptable format (can embed //