Skip to content

Commit 013f692

Browse files
waleedlatif1claude
andcommitted
fix(files): reject whitespace-only base64 payloads
Normalization strips whitespace, so a nonempty payload of only spaces or newlines collapsed to the empty encoding and was stored as a zero-byte file. Only a payload that is empty before normalization is a legitimate zero-byte file, so compare against the value after the data: URI prefix is removed — which keeps an empty `data:text/plain;base64,` accepted. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VxZCGfFjZnokR8uFeWrfLp
1 parent a095720 commit 013f692

2 files changed

Lines changed: 45 additions & 21 deletions

File tree

apps/sim/executor/utils/file-tool-processor.test.ts

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,16 @@ describe('FileToolProcessor', () => {
192192
expect(mockUploadExecutionFile.mock.calls[0]?.[1]).toEqual(Buffer.from('Hello, world!'))
193193
})
194194

195+
it('stores an empty base64 data URI as a zero-byte file', async () => {
196+
await FileToolProcessor.processToolOutputs(
197+
{ file: { name: 'empty.txt', mimeType: 'text/plain', data: 'data:text/plain;base64,' } },
198+
toolConfig,
199+
executionContext
200+
)
201+
202+
expect(mockUploadExecutionFile.mock.calls[0]?.[1]).toEqual(Buffer.alloc(0))
203+
})
204+
195205
it('stores a successful zero-byte URL download', async () => {
196206
mockDownloadFileFromUrl.mockResolvedValue(Buffer.alloc(0))
197207

@@ -204,18 +214,23 @@ describe('FileToolProcessor', () => {
204214
expect(mockUploadExecutionFile.mock.calls[0]?.[1]).toEqual(Buffer.alloc(0))
205215
})
206216

207-
it.each([undefined, null, '!!!', 'a!b!c!AAAA', 'AAAAA', { type: 'Buffer', data: 'invalid' }])(
208-
'does not turn missing or malformed data into an empty file: %j',
209-
async (data) => {
210-
await expect(
211-
FileToolProcessor.processToolOutputs(
212-
{ file: { name: 'invalid.txt', mimeType: 'text/plain', data } },
213-
toolConfig,
214-
executionContext
215-
)
216-
).rejects.toThrow("Failed to process file output 'file'")
217-
218-
expect(mockUploadExecutionFile).not.toHaveBeenCalled()
219-
}
220-
)
217+
it.each([
218+
undefined,
219+
null,
220+
'!!!',
221+
'a!b!c!AAAA',
222+
'AAAAA',
223+
' \n\t ',
224+
{ type: 'Buffer', data: 'invalid' },
225+
])('does not turn missing or malformed data into an empty file: %j', async (data) => {
226+
await expect(
227+
FileToolProcessor.processToolOutputs(
228+
{ file: { name: 'invalid.txt', mimeType: 'text/plain', data } },
229+
toolConfig,
230+
executionContext
231+
)
232+
).rejects.toThrow("Failed to process file output 'file'")
233+
234+
expect(mockUploadExecutionFile).not.toHaveBeenCalled()
235+
})
221236
})

apps/sim/executor/utils/file-tool-processor.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,20 @@ const IMAGE_FILE_EXTENSIONS: Record<string, string> = {
1818
}
1919

2020
/**
21-
* Normalize a tool-supplied base64 payload to canonical RFC 4648 form so it can be
22-
* validated: strip a base64 `data:` URI prefix, drop the line wrapping MIME encoders
23-
* emit, translate the base64url alphabet, and restore padding unpadded encoders omit.
21+
* Strip a base64 `data:` URI prefix, leaving the encoded payload. An empty payload is
22+
* a legitimate zero-byte file; a payload that only looks empty after normalization is
23+
* not, so callers compare against what this returns rather than the raw value.
2424
*/
25-
function normalizeBase64(value: string): string {
26-
const payload = /^data:[^,]*;base64,/i.test(value) ? value.slice(value.indexOf(',') + 1) : value
25+
function stripBase64DataUri(value: string): string {
26+
return /^data:[^,]*;base64,/i.test(value) ? value.slice(value.indexOf(',') + 1) : value
27+
}
28+
29+
/**
30+
* Normalize a base64 payload to canonical RFC 4648 form so it can be validated: drop
31+
* the line wrapping MIME encoders emit, translate the base64url alphabet, and restore
32+
* the padding unpadded encoders omit.
33+
*/
34+
function normalizeBase64(payload: string): string {
2735
const compact = payload.replace(/\s/g, '').replace(/-/g, '+').replace(/_/g, '/')
2836
const remainder = compact.length % 4
2937
return remainder === 0 ? compact : compact + '='.repeat(4 - remainder)
@@ -183,11 +191,12 @@ export class FileToolProcessor {
183191
throw new Error(`Invalid serialized buffer format for ${data.name}`)
184192
}
185193
} else if (typeof data.data === 'string') {
186-
const base64Data = normalizeBase64(data.data)
194+
const payload = stripBase64DataUri(data.data)
195+
const base64Data = normalizeBase64(payload)
187196

188197
const paddingBytes = base64Data.endsWith('==') ? 2 : base64Data.endsWith('=') ? 1 : 0
189198
assertFileSize(Math.floor((base64Data.length * 3) / 4) - paddingBytes, data.name)
190-
if (!isCanonicalBase64(base64Data)) {
199+
if (!isCanonicalBase64(base64Data) || (payload.length > 0 && base64Data.length === 0)) {
191200
throw new Error(`File '${data.name}' has invalid base64 data`)
192201
}
193202
buffer = Buffer.from(base64Data, 'base64')

0 commit comments

Comments
 (0)