From c1f04ed97c1450a4853065d2926ccd9a323ddeca Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Thu, 21 May 2026 18:45:08 -0700 Subject: [PATCH] fix(files): RFC 5987 encode Content-Disposition filenames --- apps/sim/app/api/files/export/[id]/route.ts | 5 +++-- apps/sim/app/api/files/utils.ts | 2 +- apps/sim/app/api/v1/admin/folders/[id]/export/route.ts | 3 ++- apps/sim/app/api/v1/admin/workspaces/[id]/export/route.ts | 3 ++- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/apps/sim/app/api/files/export/[id]/route.ts b/apps/sim/app/api/files/export/[id]/route.ts index cccbe6af1a1..18c8aafb563 100644 --- a/apps/sim/app/api/files/export/[id]/route.ts +++ b/apps/sim/app/api/files/export/[id]/route.ts @@ -13,6 +13,7 @@ import { USE_BLOB_STORAGE } from '@/lib/uploads/config' import { downloadFile } from '@/lib/uploads/core/storage-service' import { getFileMetadataById } from '@/lib/uploads/server/metadata' import { verifyFileAccess } from '@/app/api/files/authorization' +import { encodeFilenameForHeader } from '@/app/api/files/utils' const logger = createLogger('FilesExportAPI') @@ -95,7 +96,7 @@ export const GET = withRouteHandler( status: 200, headers: { 'Content-Type': 'text/markdown; charset=utf-8', - 'Content-Disposition': `attachment; filename="${mdName}"`, + 'Content-Disposition': `attachment; ${encodeFilenameForHeader(mdName)}`, 'Content-Length': String(mdBytes.length), }, }) @@ -158,7 +159,7 @@ export const GET = withRouteHandler( status: 200, headers: { 'Content-Type': 'application/zip', - 'Content-Disposition': `attachment; filename="${zipName}"`, + 'Content-Disposition': `attachment; ${encodeFilenameForHeader(zipName)}`, 'Content-Length': String(zipBuffer.length), }, }) diff --git a/apps/sim/app/api/files/utils.ts b/apps/sim/app/api/files/utils.ts index b6b05f4cbb6..2bdf7663825 100644 --- a/apps/sim/app/api/files/utils.ts +++ b/apps/sim/app/api/files/utils.ts @@ -191,7 +191,7 @@ function getSecureFileHeaders(filename: string, originalContentType: string) { } } -function encodeFilenameForHeader(storageKey: string): string { +export function encodeFilenameForHeader(storageKey: string): string { const filename = storageKey.split('/').pop() || storageKey const hasNonAscii = /[^\x00-\x7F]/.test(filename) diff --git a/apps/sim/app/api/v1/admin/folders/[id]/export/route.ts b/apps/sim/app/api/v1/admin/folders/[id]/export/route.ts index f8e3d5dde69..fb597ca7c16 100644 --- a/apps/sim/app/api/v1/admin/folders/[id]/export/route.ts +++ b/apps/sim/app/api/v1/admin/folders/[id]/export/route.ts @@ -21,6 +21,7 @@ import { parseRequest } from '@/lib/api/server' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { exportFolderToZip, sanitizePathSegment } from '@/lib/workflows/operations/import-export' import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils' +import { encodeFilenameForHeader } from '@/app/api/files/utils' import { withAdminAuthParams } from '@/app/api/v1/admin/middleware' import { internalErrorResponse, @@ -242,7 +243,7 @@ export const GET = withRouteHandler( status: 200, headers: { 'Content-Type': 'application/zip', - 'Content-Disposition': `attachment; filename="${filename}"`, + 'Content-Disposition': `attachment; ${encodeFilenameForHeader(filename)}`, 'Content-Length': arrayBuffer.byteLength.toString(), }, }) diff --git a/apps/sim/app/api/v1/admin/workspaces/[id]/export/route.ts b/apps/sim/app/api/v1/admin/workspaces/[id]/export/route.ts index ebcdc0f2616..41b00f8377a 100644 --- a/apps/sim/app/api/v1/admin/workspaces/[id]/export/route.ts +++ b/apps/sim/app/api/v1/admin/workspaces/[id]/export/route.ts @@ -21,6 +21,7 @@ import { parseRequest } from '@/lib/api/server' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { exportWorkspaceToZip, sanitizePathSegment } from '@/lib/workflows/operations/import-export' import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils' +import { encodeFilenameForHeader } from '@/app/api/files/utils' import { withAdminAuthParams } from '@/app/api/v1/admin/middleware' import { internalErrorResponse, @@ -162,7 +163,7 @@ export const GET = withRouteHandler( status: 200, headers: { 'Content-Type': 'application/zip', - 'Content-Disposition': `attachment; filename="${filename}"`, + 'Content-Disposition': `attachment; ${encodeFilenameForHeader(filename)}`, 'Content-Length': arrayBuffer.byteLength.toString(), }, })