diff --git a/apps/desktop/electron/hardening.test.ts b/apps/desktop/electron/hardening.test.ts index 1a5852f720af..4b41962e0cdb 100644 --- a/apps/desktop/electron/hardening.test.ts +++ b/apps/desktop/electron/hardening.test.ts @@ -7,6 +7,9 @@ import { pathToFileURL } from 'node:url' import { test } from 'vitest' import { + clampDataUrlReadMaxMb, + DATA_URL_READ_DEFAULT_MAX_MB, + dataUrlReadMaxBytesFromMb, DEFAULT_FETCH_TIMEOUT_MS, encryptDesktopSecret, resolveDirectoryForIpc, @@ -24,6 +27,14 @@ async function rejectsWithCode(promise, code: string) { }) } +test('clampDataUrlReadMaxMb defaults and bounds the attach size preference', () => { + assert.equal(clampDataUrlReadMaxMb(undefined), DATA_URL_READ_DEFAULT_MAX_MB) + assert.equal(clampDataUrlReadMaxMb(0), 1) + assert.equal(clampDataUrlReadMaxMb(256), 256) + assert.equal(clampDataUrlReadMaxMb(99999), 4096) + assert.equal(dataUrlReadMaxBytesFromMb(16), 16 * 1024 * 1024) +}) + test('resolveTimeoutMs falls back to defaults and accepts overrides', () => { assert.equal(resolveTimeoutMs(undefined), DEFAULT_FETCH_TIMEOUT_MS) assert.equal(resolveTimeoutMs(0), DEFAULT_FETCH_TIMEOUT_MS) diff --git a/apps/desktop/electron/hardening.ts b/apps/desktop/electron/hardening.ts index 2d6b53310018..de15a5dca5e3 100644 --- a/apps/desktop/electron/hardening.ts +++ b/apps/desktop/electron/hardening.ts @@ -4,9 +4,30 @@ import path from 'node:path' import { fileURLToPath } from 'node:url' const DEFAULT_FETCH_TIMEOUT_MS = 15_000 -const DATA_URL_READ_MAX_BYTES = 16 * 1024 * 1024 +// Default / floor / ceiling for Desktop's data-URL file load (composer attach, +// image preview, etc.). The whole file is base64-buffered in main, so this is +// a memory guard — not a model limit. Settings → Chat takes a free-form MB +// value; 16 MB ships as default. The ceiling is only a typo guard (very large +// values can OOM / crash the app). +const DATA_URL_READ_DEFAULT_MAX_MB = 16 +const DATA_URL_READ_MIN_MAX_MB = 1 +const DATA_URL_READ_MAX_MAX_MB = 4096 const TEXT_PREVIEW_SOURCE_MAX_BYTES = 64 * 1024 * 1024 +function clampDataUrlReadMaxMb(value) { + const parsed = Number(value) + + if (!Number.isFinite(parsed)) { + return DATA_URL_READ_DEFAULT_MAX_MB + } + + return Math.min(DATA_URL_READ_MAX_MAX_MB, Math.max(DATA_URL_READ_MIN_MAX_MB, Math.round(parsed))) +} + +function dataUrlReadMaxBytesFromMb(maxMb) { + return clampDataUrlReadMaxMb(maxMb) * 1024 * 1024 +} + const SAFE_ENV_SUFFIXES = new Set(['dist', 'example', 'sample', 'template']) const SENSITIVE_EXTENSIONS = new Set(['.kdbx', '.p12', '.pem', '.pfx']) @@ -304,7 +325,11 @@ async function resolveReadableFileForIpc( } export { - DATA_URL_READ_MAX_BYTES, + clampDataUrlReadMaxMb, + DATA_URL_READ_DEFAULT_MAX_MB, + DATA_URL_READ_MAX_MAX_MB, + DATA_URL_READ_MIN_MAX_MB, + dataUrlReadMaxBytesFromMb, DEFAULT_FETCH_TIMEOUT_MS, encryptDesktopSecret, rejectUnsafePathSyntax, diff --git a/apps/desktop/electron/main.ts b/apps/desktop/electron/main.ts index 3dc41b784982..dfb4747f0392 100644 --- a/apps/desktop/electron/main.ts +++ b/apps/desktop/electron/main.ts @@ -115,7 +115,9 @@ import { switchBranch } from './git-worktree-ops' import { - DATA_URL_READ_MAX_BYTES, + clampDataUrlReadMaxMb, + DATA_URL_READ_DEFAULT_MAX_MB, + dataUrlReadMaxBytesFromMb, DEFAULT_FETCH_TIMEOUT_MS, encryptDesktopSecret as encryptDesktopSecretStrict, resolveReadableFileForIpc, @@ -960,7 +962,7 @@ app.setAboutPanelOptions({ // Custom scheme for streaming local media (video/audio) into the renderer. // Reading large media through `readFileDataUrl` failed: it base64-loads the -// whole file into memory and is hard-capped at DATA_URL_READ_MAX_BYTES (16 MB), +// whole file into memory and is hard-capped (default 16 MB, Settings → Chat), // so any non-trivial video silently refused to load. Streaming via a protocol // handler removes the size cap and gives the