Skip to content

Commit 135512e

Browse files
committed
address comments
1 parent 3cc2692 commit 135512e

4 files changed

Lines changed: 64 additions & 5 deletions

File tree

apps/sim/lib/uploads/utils/file-utils.server.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
isInternalFileUrl,
2323
processSingleFileToUserFile,
2424
type RawFileInput,
25+
resolveTrustedFileContext,
2526
} from '@/lib/uploads/utils/file-utils'
2627
import { verifyFileAccess } from '@/app/api/files/authorization'
2728
import type { UserFile } from '@/executor/types'
@@ -89,7 +90,7 @@ export async function resolveFileInputToUrl(
8990

9091
// Generate presigned URL if we have a key but no URL
9192
if (!fileUrl && userFile.key) {
92-
const context = inferContextFromKey(userFile.key)
93+
const context = resolveTrustedFileContext(userFile.key, userFile.context)
9394
const hasAccess = await verifyFileAccess(userFile.key, userId, undefined, context, false)
9495

9596
if (!hasAccess) {
@@ -280,7 +281,7 @@ export async function downloadFileFromStorage(
280281
)
281282
buffer = await downloadExecutionFile(userFile, { maxBytes: options.maxBytes })
282283
} else if (userFile.key) {
283-
const context = inferContextFromKey(userFile.key)
284+
const context = resolveTrustedFileContext(userFile.key, userFile.context)
284285
logger.info(`[${requestId}] Downloading from ${context} storage: ${userFile.key}`)
285286

286287
const { downloadFile } = await import('@/lib/uploads/core/storage-service')

apps/sim/lib/uploads/utils/file-utils.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
isInternalFileUrl,
1010
isNetworkError,
1111
processSingleFileToUserFile,
12+
resolveTrustedFileContext,
1213
} from '@/lib/uploads/utils/file-utils'
1314

1415
const logger = createLogger('FileUtilsTest')
@@ -74,6 +75,26 @@ describe('inferContextFromKey', () => {
7475
})
7576
})
7677

78+
describe('resolveTrustedFileContext', () => {
79+
it('derives from the key prefix and ignores a mismatched caller context', () => {
80+
expect(resolveTrustedFileContext('workspace/ws/1700000000000-abc-x.pdf', 'og-images')).toBe(
81+
'workspace'
82+
)
83+
expect(resolveTrustedFileContext('chat/x', 'workspace-logos')).toBe('chat')
84+
expect(resolveTrustedFileContext('workspace/ws/x', 'mothership')).toBe('workspace')
85+
})
86+
87+
it('honors the caller context for legacy keys with no inferrable prefix', () => {
88+
expect(resolveTrustedFileContext('legacy/ws/wf/ex/report.pdf', 'execution')).toBe('execution')
89+
})
90+
91+
it('never resolves an un-inferrable key to a world-readable context', () => {
92+
expect(() => resolveTrustedFileContext('legacy/report.pdf', 'og-images')).toThrow()
93+
expect(() => resolveTrustedFileContext('legacy/report.pdf', 'profile-pictures')).toThrow()
94+
expect(() => resolveTrustedFileContext('legacy/report.pdf')).toThrow()
95+
})
96+
})
97+
7798
describe('isAbortError', () => {
7899
it('returns true for AbortError-named errors', () => {
79100
const err = new Error('aborted')

apps/sim/lib/uploads/utils/file-utils.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -596,6 +596,43 @@ export function inferContextFromKey(key: string): StorageContext {
596596
)
597597
}
598598

599+
/**
600+
* World-readable storage contexts. Reads for these short-circuit file
601+
* authorization and can resolve to the shared bucket, so a caller-supplied
602+
* context must never select one for a key that does not carry the matching
603+
* prefix.
604+
*/
605+
const PUBLIC_STORAGE_CONTEXTS = new Set<StorageContext>([
606+
'profile-pictures',
607+
'og-images',
608+
'workspace-logos',
609+
])
610+
611+
/**
612+
* Resolve the storage context for a stored file from its trusted key prefix.
613+
*
614+
* The storage key is written server-side at upload time and cannot be forged to
615+
* change tenant, whereas a file's `context` field is attacker-authorable in a
616+
* workflow. When the key carries a recognized prefix that prefix is
617+
* authoritative and the caller-supplied `context` is ignored — this prevents a
618+
* private `workspace/…` key from being relabeled with a world-readable context
619+
* to bypass authorization and read the shared bucket.
620+
*
621+
* Legacy keys predating context-prefixed keys cannot be inferred; for those the
622+
* persisted `context` is honored so existing files stay resolvable — except a
623+
* world-readable context, which would reopen the bypass on an un-inferrable key.
624+
*/
625+
export function resolveTrustedFileContext(key: string, context?: string): StorageContext {
626+
try {
627+
return inferContextFromKey(key)
628+
} catch (error) {
629+
if (context && !PUBLIC_STORAGE_CONTEXTS.has(context as StorageContext)) {
630+
return context as StorageContext
631+
}
632+
throw error
633+
}
634+
}
635+
599636
/**
600637
* Extract storage key and context from an internal file URL
601638
* @param fileUrl - Internal file URL (e.g., /api/files/serve/key?context=workspace)

apps/sim/providers/file-attachments.server.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { createLogger } from '@sim/logger'
33
import { getErrorMessage } from '@sim/utils/errors'
44
import { sleep } from '@sim/utils/helpers'
55
import { StorageService } from '@/lib/uploads'
6-
import { inferContextFromKey } from '@/lib/uploads/utils/file-utils'
6+
import { resolveTrustedFileContext } from '@/lib/uploads/utils/file-utils'
77
import { downloadServableFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
88
import { verifyFileAccess } from '@/app/api/files/authorization'
99
import type { UserFile } from '@/executor/types'
@@ -84,7 +84,7 @@ export async function attachLargeFileRemoteUrls(
8484
)
8585
}
8686

87-
const context = inferContextFromKey(file.key)
87+
const context = resolveTrustedFileContext(file.key, file.context)
8888
const hasAccess = await verifyFileAccess(file.key, request.userId, undefined, context, false)
8989
if (!hasAccess) {
9090
throw new Error(`File "${file.name}" is not accessible for provider "${providerId}"`)
@@ -146,7 +146,7 @@ async function assertFileAccessForUpload(
146146
if (!userId) {
147147
throw new Error(`File "${file.name}" requires an authenticated user to upload`)
148148
}
149-
const context = inferContextFromKey(file.key)
149+
const context = resolveTrustedFileContext(file.key, file.context)
150150
const hasAccess = await verifyFileAccess(file.key, userId, undefined, context, false)
151151
if (!hasAccess) {
152152
throw new Error(`File "${file.name}" is not accessible`)

0 commit comments

Comments
 (0)