From 629aea2608595bfbf220dee4979a7f890e99c0ac Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Sun, 11 Oct 2026 12:26:03 +0200 Subject: [PATCH] fix(app): enforce the session download size limit during the read The token size is sampled before the file is read. Pass the download byte budget through the actual read so a changed file cannot bypass the in-memory limit. Co-authored-by: Michael Vandesompel Co-authored-by: YASICJUNWOO Co-authored-by: JrDw0 --- .../src/hooks/use-file-explorer-actions.ts | 4 ++-- .../app/src/stores/download-store.test.ts | 23 +++++++++++++++++++ packages/app/src/stores/download-store.ts | 4 ++-- 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/packages/app/src/hooks/use-file-explorer-actions.ts b/packages/app/src/hooks/use-file-explorer-actions.ts index 42b22964fe..c29feb5241 100644 --- a/packages/app/src/hooks/use-file-explorer-actions.ts +++ b/packages/app/src/hooks/use-file-explorer-actions.ts @@ -254,7 +254,7 @@ export function useFileExplorerActions(params: { serverId: string } & FileExplor ); const requestFileBytes = useCallback( - async (path: string) => { + async (path: string, maxBytes: number) => { if (!normalizedWorkspaceRoot) { throw new Error(t("workspace.fileExplorer.states.unavailable")); } @@ -263,7 +263,7 @@ export function useFileExplorerActions(params: { serverId: string } & FileExplor } // No request timeout: a large file over a slow relay legitimately takes longer than the // 60 s default, and a dead connection still rejects on disconnect. - return client.readFile(normalizedWorkspaceRoot, path, undefined, undefined, 0); + return client.readFile(normalizedWorkspaceRoot, path, undefined, maxBytes, 0); }, [client, normalizedWorkspaceRoot, t], ); diff --git a/packages/app/src/stores/download-store.test.ts b/packages/app/src/stores/download-store.test.ts index 75ffb1dcf0..d3657b4c34 100644 --- a/packages/app/src/stores/download-store.test.ts +++ b/packages/app/src/stores/download-store.test.ts @@ -142,6 +142,29 @@ describe("download store transport selection", () => { ]); }); + it("bounds the session read even when the token reports a smaller file", async () => { + const readRequests: Array<{ path: string; maxBytes: number }> = []; + const saver = createFileSaver(); + await useDownloadStore.getState().startDownload({ + serverId: "srv_remote", + scopeId: "workspace-1", + fileName: FILE_NAME, + path: FILE_PATH, + daemonProfile: profileWith([relayConnection]), + activeConnectionId: relayConnection.id, + requestFileDownloadToken: createTokenRequest(1).request, + readFile: async (path: string, maxBytes: number) => { + readRequests.push({ path, maxBytes }); + return { bytes: FILE_BYTES, size: FILE_BYTES.byteLength }; + }, + saveFile: saver.save, + }); + expect(readRequests).toEqual([{ path: FILE_PATH, maxBytes: MAX_SESSION_DOWNLOAD_BYTES }]); + expect(saver.savedFiles).toEqual([ + { bytes: FILE_BYTES, fileName: FILE_NAME, mimeType: MIME_TYPE }, + ]); + }); + it("ignores a directTcp entry that is not the active connection", async () => { const reader = createFileReader(async () => ({ bytes: FILE_BYTES, diff --git a/packages/app/src/stores/download-store.ts b/packages/app/src/stores/download-store.ts index 3b9935a86e..790fbd7dee 100644 --- a/packages/app/src/stores/download-store.ts +++ b/packages/app/src/stores/download-store.ts @@ -57,7 +57,7 @@ interface DownloadState { error: string | null; }>; /** Reads the whole file over the session socket; used when no direct HTTP endpoint is active. */ - readFile: (path: string) => Promise<{ bytes: Uint8Array; size: number }>; + readFile: (path: string, maxBytes: number) => Promise<{ bytes: Uint8Array; size: number }>; saveFile?: typeof saveDownloadedBytes; }) => Promise; @@ -121,7 +121,7 @@ export const useDownloadStore = create()((set, get) => ({ i18n.t("composer.errors.fileTooLarge", { fileName: resolvedFileName, size: "64 MB" }), ); } - const { bytes, size } = await readFile(path); + const { bytes, size } = await readFile(path, MAX_SESSION_DOWNLOAD_BYTES); // A daemon without a binary channel answers `encoding: "none"` for a binary file: no // content, real size. Never save that as a complete download. if (bytes.byteLength !== size) {