From dc3a8452c6a87f90f34b9b28eff16d8fe018a495 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Sun, 30 Aug 2026 16:12:33 -0400 Subject: [PATCH] fix(artifacts): enforce recovery content limit --- .../artifact-create-intent-store.test.ts | 26 ++++++++++++++++--- .../artifacts/artifact-create-intent-store.ts | 10 ++++++- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/src/main/artifacts/artifact-create-intent-store.test.ts b/src/main/artifacts/artifact-create-intent-store.test.ts index 32da4e1a6cd..2888ef19042 100644 --- a/src/main/artifacts/artifact-create-intent-store.test.ts +++ b/src/main/artifacts/artifact-create-intent-store.test.ts @@ -3,7 +3,11 @@ import { mkdtemp, readFile, readdir, rm, stat, truncate, writeFile } from 'node: import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, describe, expect, it, vi } from 'vitest' -import { ARTIFACT_MAX_REQUEST_BYTES, artifactWriteRequestByteLength } from '../../shared/artifacts' +import { + ARTIFACT_MAX_CONTENT_BYTES, + ARTIFACT_MAX_REQUEST_BYTES, + artifactWriteRequestByteLength +} from '../../shared/artifacts' import { MAX_ARTIFACT_CREATE_INTENT_BYTES, MAX_PENDING_ARTIFACT_CREATES, @@ -270,9 +274,9 @@ describe('artifact create intent store', () => { ).toThrow(/unsupported format/) }) - it('persists a valid artifact request near the recovery limit', async () => { + it('persists a 5 MiB escaped artifact within the recovery limit', async () => { const userDataPath = await createUserDataPath() - const nearLimitBody = { ...body, content: 'x'.repeat(ARTIFACT_MAX_REQUEST_BYTES - 200) } + const nearLimitBody = { ...body, content: '"'.repeat(ARTIFACT_MAX_CONTENT_BYTES) } expect( artifactWriteRequestByteLength({ sourceKey: '/repo/report.html', ...nearLimitBody }) ).toBeLessThanOrEqual(ARTIFACT_MAX_REQUEST_BYTES) @@ -289,7 +293,21 @@ describe('artifact create intent store', () => { ).not.toThrow() const directory = join(userDataPath, 'profiles', 'local-profile', 'artifact-create-intents') const [fileName] = await readdir(directory) - expect((await stat(join(directory, fileName))).size).toBeGreaterThan(ARTIFACT_MAX_REQUEST_BYTES) + expect((await stat(join(directory, fileName))).size).toBeGreaterThan(ARTIFACT_MAX_CONTENT_BYTES) + }) + + it('rejects oversized artifact content before creating a recovery record', async () => { + const userDataPath = await createUserDataPath() + expect(() => + getOrCreateArtifactCreateIntent( + 'local-profile', + userDataPath, + '/repo/report.html', + scope, + 'key-a', + { ...body, content: 'x'.repeat(ARTIFACT_MAX_CONTENT_BYTES + 1) } + ) + ).toThrow(/5 MiB limit/) }) it('rejects an oversized recovery record before reading it', async () => { diff --git a/src/main/artifacts/artifact-create-intent-store.ts b/src/main/artifacts/artifact-create-intent-store.ts index b463ad4ce10..ba547c1453a 100644 --- a/src/main/artifacts/artifact-create-intent-store.ts +++ b/src/main/artifacts/artifact-create-intent-store.ts @@ -10,7 +10,11 @@ import { writeFileSync } from 'node:fs' import { join } from 'node:path' -import { ARTIFACT_MAX_REQUEST_BYTES } from '../../shared/artifacts' +import { + ARTIFACT_MAX_CONTENT_BYTES, + ARTIFACT_MAX_REQUEST_BYTES, + artifactContentByteLength +} from '../../shared/artifacts' import { bestEffortFsyncDirectorySync, fsyncFileSync, @@ -116,6 +120,7 @@ function isWriteBody(value: unknown): value is ArtifactWriteBody { const body = value as Partial return ( typeof body.content === 'string' && + artifactContentByteLength(body.content) <= ARTIFACT_MAX_CONTENT_BYTES && (body.contentType === 'text/html' || body.contentType === 'text/markdown') && typeof body.fileName === 'string' && (body.title === undefined || typeof body.title === 'string') @@ -193,6 +198,9 @@ export function getOrCreateArtifactCreateIntent( idempotencyKey: string, body: ArtifactWriteBody ): ArtifactCreateIntent { + if (artifactContentByteLength(body.content) > ARTIFACT_MAX_CONTENT_BYTES) { + throw new Error('Artifact content exceeds the 5 MiB limit.') + } const existing = getArtifactCreateIntent(profileId, userDataPath, sourceKey, scope) if (existing) { return existing