Skip to content

Commit 986c294

Browse files
committed
fix(file): exclude skipped entries from caps and reject multi-archive decompress
- Resolve safe (sanitized) zip entries up front so unsafe/skipped entries no longer count toward the per-entry and total uncompressed-size caps (cursor) - Reject decompress input that resolves to more than one archive with a clear error instead of silently extracting only the first (cursor)
1 parent aae3d46 commit 986c294

2 files changed

Lines changed: 27 additions & 14 deletions

File tree

apps/sim/app/api/tools/file/manage/route.ts

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -746,10 +746,24 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
746746
{ status: 413 }
747747
)
748748

749+
// Resolve which entries are safe to extract first, so unsafe entries
750+
// (skipped below) never count toward the size caps.
751+
const safeEntries: Array<{ entry: JSZip.JSZipObject; segments: string[] }> = []
752+
let skippedCount = 0
753+
for (const entry of entries) {
754+
const segments = sanitizeArchiveEntryPath(entry.name)
755+
if (!segments) {
756+
skippedCount += 1
757+
logger.warn('Skipping unsafe archive entry', { name: entry.name })
758+
continue
759+
}
760+
safeEntries.push({ entry, segments })
761+
}
762+
749763
// Reject standard zip bombs up front using the declared uncompressed sizes,
750764
// before materializing any entry into memory.
751765
let declaredTotal = 0
752-
for (const entry of entries) {
766+
for (const { entry } of safeEntries) {
753767
const declaredSize = readEntryUncompressedSize(entry)
754768
if (declaredSize === undefined) continue
755769
if (declaredSize > MAX_DECOMPRESS_ENTRY_BYTES) return entryTooLargeResponse(entry.name)
@@ -760,16 +774,8 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
760774
// Read and validate every safe entry before writing anything, so a cap
761775
// breach never leaves partially-extracted files behind in the workspace.
762776
const pending: Array<{ segments: string[]; buffer: Buffer }> = []
763-
let skippedCount = 0
764777
let totalBytes = 0
765-
for (const entry of entries) {
766-
const segments = sanitizeArchiveEntryPath(entry.name)
767-
if (!segments) {
768-
skippedCount += 1
769-
logger.warn('Skipping unsafe archive entry', { name: entry.name })
770-
continue
771-
}
772-
778+
for (const { entry, segments } of safeEntries) {
773779
const buffer = await entry.async('nodebuffer')
774780
// Enforce the per-entry cap on the materialized size too, covering
775781
// entries that omit a declared uncompressed size.

apps/sim/blocks/blocks/file.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1107,19 +1107,26 @@ export const FileV5Block: BlockConfig<FileParserV3Output> = {
11071107

11081108
const fileIds = parseReadFileIds(decompressInput)
11091109
if (fileIds) {
1110+
const ids = Array.isArray(fileIds) ? fileIds : [fileIds]
1111+
if (ids.length > 1) {
1112+
throw new Error('Decompress accepts a single .zip archive at a time')
1113+
}
11101114
return {
1111-
fileId: Array.isArray(fileIds) ? fileIds[0] : fileIds,
1115+
fileId: ids[0],
11121116
workspaceId: params._context?.workspaceId,
11131117
}
11141118
}
11151119

1116-
const normalized = normalizeFileInput(decompressInput, { single: true })
1117-
if (!normalized) {
1120+
const normalized = normalizeFileInput(decompressInput)
1121+
if (!normalized || normalized.length === 0) {
11181122
throw new Error('File is required for decompress')
11191123
}
1124+
if (normalized.length > 1) {
1125+
throw new Error('Decompress accepts a single .zip archive at a time')
1126+
}
11201127

11211128
return {
1122-
fileInput: normalized,
1129+
fileInput: normalized[0],
11231130
workspaceId: params._context?.workspaceId,
11241131
}
11251132
}

0 commit comments

Comments
 (0)