From 59daeeb00970a729a7e237719da1d0dad7fb52af Mon Sep 17 00:00:00 2001 From: tomaioo Date: Fri, 24 Apr 2026 11:18:35 -0700 Subject: [PATCH 1/2] fix(security): 2 improvements across 2 files - Security: TOCTOU race in defensive file reader can bypass type/size checks - Security: Unbounded IPC payload size for export can enable renderer-to-main DoS Signed-off-by: tomaioo <203048277+tomaioo@users.noreply.github.com> --- apps/desktop/src/main/imports/safe-read.ts | 59 ++++++++++++++-------- 1 file changed, 38 insertions(+), 21 deletions(-) diff --git a/apps/desktop/src/main/imports/safe-read.ts b/apps/desktop/src/main/imports/safe-read.ts index 75480530..4e40b6a0 100644 --- a/apps/desktop/src/main/imports/safe-read.ts +++ b/apps/desktop/src/main/imports/safe-read.ts @@ -1,4 +1,4 @@ -import { readFile, stat } from 'node:fs/promises'; +import { open, stat } from 'node:fs/promises'; import { getLogger } from '../logger'; /** @@ -11,9 +11,10 @@ import { getLogger } from '../logger'; * - plant a 10-GB file at the same path and exhaust the heap; * - point the path at a socket / FIFO / directory to hang `readFile`. * - * `safeReadImportFile` stats first, rejects anything that isn't a regular - * file or that exceeds `MAX_IMPORT_FILE_BYTES`, and falls through to - * `readFile` only when the stat says it's safe. Returns `null` on + * `safeReadImportFile` opens the path once, stats via that handle, + * rejects anything that isn't a regular file or that exceeds + * `MAX_IMPORT_FILE_BYTES`, and reads only when the stat says it's safe. + * Returns `null` on * missing/too-big/not-a-file so callers keep their existing "no config * found" branches; logs the reason so diagnostics bundles capture a * rejection trail. @@ -26,28 +27,44 @@ const log = getLogger('import-read'); export const MAX_IMPORT_FILE_BYTES = 256 * 1024; export async function safeReadImportFile(path: string): Promise { - let stats: Awaited>; + let fileHandle: Awaited>; try { - stats = await stat(path); + fileHandle = await open(path, 'r'); } catch (err) { const code = (err as NodeJS.ErrnoException).code; if (code === 'ENOENT') return null; - log.warn('safe_read.stat_failed', { path, code: code ?? 'unknown' }); + log.warn('safe_read.open_failed', { path, code: code ?? 'unknown' }); return null; } - if (!stats.isFile()) { - // Symlink to /dev/zero, symlink to a directory, named pipe, etc. - // `isFile()` follows symlinks, so a symlink to a regular file is fine. - log.warn('safe_read.not_regular_file', { path }); - return null; - } - if (stats.size > MAX_IMPORT_FILE_BYTES) { - log.warn('safe_read.size_exceeded', { - path, - size: stats.size, - cap: MAX_IMPORT_FILE_BYTES, - }); - return null; + + try { + let stats: Awaited>; + try { + stats = await fileHandle.stat(); + } catch (err) { + const code = (err as NodeJS.ErrnoException).code; + if (code === 'ENOENT') return null; + log.warn('safe_read.stat_failed', { path, code: code ?? 'unknown' }); + return null; + } + + if (!stats.isFile()) { + // Symlink to /dev/zero, symlink to a directory, named pipe, etc. + // `isFile()` follows symlinks, so a symlink to a regular file is fine. + log.warn('safe_read.not_regular_file', { path }); + return null; + } + if (stats.size > MAX_IMPORT_FILE_BYTES) { + log.warn('safe_read.size_exceeded', { + path, + size: stats.size, + cap: MAX_IMPORT_FILE_BYTES, + }); + return null; + } + + return fileHandle.readFile('utf8'); + } finally { + await fileHandle.close(); } - return readFile(path, 'utf8'); } From c7a9ff4957afb12c2b3a19633fbbe53a0b7ad42e Mon Sep 17 00:00:00 2001 From: tomaioo Date: Fri, 24 Apr 2026 11:18:36 -0700 Subject: [PATCH 2/2] fix(security): 2 improvements across 2 files - Security: TOCTOU race in defensive file reader can bypass type/size checks - Security: Unbounded IPC payload size for export can enable renderer-to-main DoS Signed-off-by: tomaioo <203048277+tomaioo@users.noreply.github.com> --- apps/desktop/src/main/exporter-ipc.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/desktop/src/main/exporter-ipc.ts b/apps/desktop/src/main/exporter-ipc.ts index 64cf3524..59e58de9 100644 --- a/apps/desktop/src/main/exporter-ipc.ts +++ b/apps/desktop/src/main/exporter-ipc.ts @@ -11,6 +11,8 @@ const FORMAT_FILTERS: Record = { markdown: [{ name: 'Markdown', extensions: ['md'] }], }; +const MAX_HTML_CONTENT_BYTES = 10 * 1024 * 1024; + export interface ExportRequest { format: ExporterFormat; htmlContent: string; @@ -46,6 +48,9 @@ export function parseRequest(raw: unknown): ExportRequest { if (typeof html !== 'string' || html.length === 0) { throw new CodesignError('export requires non-empty htmlContent', ERROR_CODES.IPC_BAD_INPUT); } + if (Buffer.byteLength(html, 'utf8') > MAX_HTML_CONTENT_BYTES) { + throw new CodesignError('export htmlContent exceeds size limit', ERROR_CODES.IPC_BAD_INPUT); + } const out: ExportRequest = { format, htmlContent: html }; if (typeof defaultFilename === 'string' && defaultFilename.length > 0) { out.defaultFilename = defaultFilename;