Skip to content

Commit db9cdc8

Browse files
fix(tables): guard sync import overlap, scope fileKey to workspace, delete-on-replace after download
1 parent 9284acc commit db9cdc8

4 files changed

Lines changed: 22 additions & 2 deletions

File tree

apps/sim/app/api/table/[tableId]/import-async/route.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ export const POST = withRouteHandler(async (request: NextRequest, { params }: Ro
4141
if (table.workspaceId !== workspaceId) {
4242
return NextResponse.json({ error: 'Invalid workspace ID' }, { status: 400 })
4343
}
44+
// The fileKey is client-supplied — ensure it points at this workspace's storage prefix so a
45+
// caller can't import another workspace's uploaded object.
46+
if (!fileKey.startsWith(`workspace/${workspaceId}/`)) {
47+
return NextResponse.json({ error: 'Invalid file key for workspace' }, { status: 400 })
48+
}
4449
if (table.archivedAt) {
4550
return NextResponse.json({ error: 'Cannot import into an archived table' }, { status: 400 })
4651
}

apps/sim/app/api/table/[tableId]/import/route.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,14 @@ export const POST = withRouteHandler(async (request: NextRequest, { params }: Ro
128128
if (table.archivedAt) {
129129
return NextResponse.json({ error: 'Cannot import into an archived table' }, { status: 400 })
130130
}
131+
// Don't run a sync import on top of an in-flight background import — concurrent writers
132+
// would insert at colliding row positions.
133+
if (table.importStatus === 'importing') {
134+
return NextResponse.json(
135+
{ error: 'An import is already in progress for this table' },
136+
{ status: 409 }
137+
)
138+
}
131139

132140
let mapping: CsvHeaderMapping | undefined
133141
if (fields.mapping) {

apps/sim/app/api/table/import-async/route.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
4040
if (permission !== 'write' && permission !== 'admin') {
4141
return NextResponse.json({ error: 'Access denied' }, { status: 403 })
4242
}
43+
// The fileKey is client-supplied — ensure it points at this workspace's storage prefix so a
44+
// caller can't import another workspace's uploaded object.
45+
if (!fileKey.startsWith(`workspace/${workspaceId}/`)) {
46+
return NextResponse.json({ error: 'Invalid file key for workspace' }, { status: 400 })
47+
}
4348

4449
const ext = fileName.split('.').pop()?.toLowerCase()
4550
if (ext !== 'csv' && ext !== 'tsv') {

apps/sim/lib/table/import-runner.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,12 @@ export async function runTableImport(payload: TableImportPayload): Promise<void>
7070
if (!loaded) throw new Error(`Import target table ${tableId} not found`)
7171
const table = loaded
7272

73-
if (mode === 'replace') await deleteAllTableRows(tableId)
74-
7573
const buffer = await downloadFile({ key: fileKey, context: 'workspace' })
7674

75+
// Delete only after the download succeeds — otherwise a failed download would wipe the
76+
// table with nothing to replace it with.
77+
if (mode === 'replace') await deleteAllTableRows(tableId)
78+
7779
// Estimate total data rows by counting line breaks (minus the header) for a
7880
// determinate progress bar. It's an estimate — quoted newlines and blank lines
7981
// make it imprecise — so the client caps the bar below 100% until the terminal

0 commit comments

Comments
 (0)