Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
109 commits
Select commit Hold shift + click to select a range
0b9019d
v0.6.23: MCP fixes, remove local state in favor of server state, moth…
waleedlatif1 Apr 4, 2026
a54dcbe
v0.6.24: copilot feedback wiring, captcha fixes
waleedlatif1 Apr 4, 2026
28af223
v0.6.25: cloudwatch, cloudformation, live kb sync, linear fixes, post…
waleedlatif1 Apr 5, 2026
d889f32
v0.6.26: ui improvements, multiple response blocks, docx previews, ol…
waleedlatif1 Apr 5, 2026
316bc8c
v0.6.27: new triggers, mothership improvements, files archive, queuei…
waleedlatif1 Apr 7, 2026
3f508e4
v0.6.28: new docs, delete confirmation standardization, dagster integ…
waleedlatif1 Apr 7, 2026
d6ec115
v0.6.29: login improvements, posthog telemetry (#4026)
TheodoreSpeaks Apr 7, 2026
d7da35b
v0.6.30: slack trigger enhancements, connectors performance improveme…
waleedlatif1 Apr 8, 2026
cf233bb
v0.6.31: elevenlabs voice, trigger.dev fixes, cloud whitelabeling for…
waleedlatif1 Apr 8, 2026
f8f3758
v0.6.32: BYOK fixes, ui improvements, cloudwatch tools, jsm tools ext…
waleedlatif1 Apr 9, 2026
3c8bb40
v0.6.33: polling improvements, jsm forms tools, credentials reactquer…
waleedlatif1 Apr 9, 2026
d33acf4
v0.6.34: trigger.dev fixes, CI speedup, atlassian error extractor
waleedlatif1 Apr 9, 2026
4f40c4c
v0.6.35: additional jira fields, HITL docs, logs cleanup efficiency
waleedlatif1 Apr 10, 2026
cbfab1c
v0.6.36: new chunkers, sockets state machine, google sheets/drive/cal…
waleedlatif1 Apr 11, 2026
4309d06
v0.6.37: audit logs page, isolated-vm worker rotation, permission gro…
waleedlatif1 Apr 12, 2026
8b57476
v0.6.38: models page
waleedlatif1 Apr 12, 2026
e3d0e74
v0.6.39: billing fixes, tools audit, landing fix
waleedlatif1 Apr 13, 2026
0ac0539
v0.6.40: mothership tool loop, new skills, agiloft, STS, IAM integrat…
waleedlatif1 Apr 14, 2026
3838b6e
v0.6.41: webhooks fix, workers removal
waleedlatif1 Apr 14, 2026
fc07922
v0.6.42: mothership nested file reads, search modal improvements
waleedlatif1 Apr 14, 2026
3a1b1a8
v0.6.43: mothership billing idempotency, env var resolution fixes
waleedlatif1 Apr 14, 2026
46ffc49
v0.6.44: streamdown, mothership intelligence, excel extension
waleedlatif1 Apr 15, 2026
010435c
v0.6.45: superagent, csp, brightdata integration, gemini response for…
Sg312 Apr 15, 2026
c0bc62c
Merge pull request #4190 from simstudioai/staging
icecrasher321 Apr 16, 2026
387cc97
v0.6.46: mothership queueing, web vitals
waleedlatif1 Apr 16, 2026
2dbc7fd
v0.6.47: files focusing, documentation, opus 4.7
waleedlatif1 Apr 16, 2026
8a50f18
v0.6.48: import csv into tables, subflow fixes, CSP updates
waleedlatif1 Apr 16, 2026
dcf3302
v0.6.49: deploy sockets event, resolver, logs improvements, monday.co…
waleedlatif1 Apr 17, 2026
bc09865
v0.6.50: ppt/doc/pdf worker isolation, docs, chat, sidebar improvements
icecrasher321 Apr 18, 2026
5f56e46
v0.6.51: tables improvements, billing fixes, 404 pages, code hygiene
waleedlatif1 Apr 20, 2026
ca3bbf1
v0.6.52: data retention, docs updates, slack manifest generator, secu…
waleedlatif1 Apr 22, 2026
bbf400f
v0.6.53: permissions groups migration, docs updates
waleedlatif1 Apr 22, 2026
7c619e7
Merge pull request #4261 from simstudioai/staging
icecrasher321 Apr 22, 2026
64cfda5
v0.6.54: mothership tracing, db pool size increase
icecrasher321 Apr 22, 2026
7ca736a
v0.6.55: standardize monorepo conventions, api key hash, thinking tex…
waleedlatif1 Apr 23, 2026
6066fc1
v0.6.56: data retention improvements, tables column double click resi…
waleedlatif1 Apr 24, 2026
3422f64
Merge pull request #4285 from simstudioai/staging
waleedlatif1 Apr 24, 2026
595c4c3
Merge pull request #4293 from simstudioai/staging
TheodoreSpeaks Apr 24, 2026
d6c1bc2
v0.6.58: queue abort state machine improvement, contributing guide
icecrasher321 Apr 25, 2026
58a3ae2
v0.6.59: gpt 5.5, security hardening, parallel subagents rendering
icecrasher321 Apr 27, 2026
489f2d3
v0.6.60: copilot security improvements, slack canvas ops, retention j…
icecrasher321 Apr 27, 2026
6aa3fe3
v0.6.61: SAP integration, live URLs for browser use, 5xx error catego…
icecrasher321 Apr 29, 2026
ecbf5e5
Merge pull request #4342 from simstudioai/staging
TheodoreSpeaks Apr 29, 2026
2aaf2b7
v0.6.62: firecrawl parse, new gmail tools, trace improvements, tool f…
waleedlatif1 May 2, 2026
d445b9c
v0.6.63: knowledgebase UI, folder search in mothership
waleedlatif1 May 2, 2026
4bc6a17
v0.6.64: table limits env vars, workspace files improvements, integra…
waleedlatif1 May 3, 2026
5be12f8
v0.6.65: memory fix, image uploads in files
waleedlatif1 May 3, 2026
4253e57
v0.6.66: child trace spans, reranker controls, attachment previews, l…
waleedlatif1 May 5, 2026
8d6b615
v0.6.67: VFS upload fix, posthog/copilot correlation, exa date filter…
TheodoreSpeaks May 5, 2026
efcd51a
v0.6.68: atlassian service accounts, 30 day wait block, markdown rend…
waleedlatif1 May 6, 2026
8d934f3
v0.6.69: security hardening, nextjs upgrade, SAP Concur, Emailbison i…
waleedlatif1 May 7, 2026
5ea80a8
v0.6.70: legacy workflow sanitization
icecrasher321 May 7, 2026
3cc581e
v0.6.71: build error fix
icecrasher321 May 7, 2026
273e608
Merge pull request #4496 from simstudioai/staging
TheodoreSpeaks May 7, 2026
07b8f1b
v0.6.72: tables improvements, search and replace, logs with files, im…
waleedlatif1 May 9, 2026
dcaf3e9
v0.6.73: zustand v5 migration fix
icecrasher321 May 9, 2026
6aeb981
v0.6.74: security hardening, workers recycling, next-mdx-remote and o…
waleedlatif1 May 12, 2026
3e9849b
v0.6.75: scheduler claim-budget drain, helm chart hardening, mothersh…
TheodoreSpeaks May 12, 2026
64d855a
v0.6.76: helm updates, media centering, lazy loading, security hardening
waleedlatif1 May 13, 2026
ab156b5
v0.6.77: mothership improvements, trigger.dev telemetry
icecrasher321 May 14, 2026
c09a2c9
v0.6.78: file block get
Sg312 May 14, 2026
6a5eebc
v0.6.79: rate limits, tables checkboxes, drizzle config changes, bill…
waleedlatif1 May 14, 2026
4efe999
v0.6.80: security hardening, nextjs minor version bump, cloudwatch to…
waleedlatif1 May 15, 2026
f69a9a0
v0.6.81: files in agent block, file block update, mermaid version upd…
waleedlatif1 May 16, 2026
db7f1c1
v0.6.82: fix duplicate migration
Sg312 May 16, 2026
dbe8e51
v0.6.83: redis TLS SNI override for IP-based REDIS_URL, zod schema fixes
TheodoreSpeaks May 17, 2026
11bcb8f
v0.6.84: redis pub/sub SNI override, security hardening, copilot read…
TheodoreSpeaks May 17, 2026
d14af04
v0.6.85: mothership stream, resource column spacing, prospeo, findyma…
waleedlatif1 May 19, 2026
e6b3cce
v0.6.86: gemini 3.5 flash, wiza integration, CORS cleanup, railway an…
waleedlatif1 May 20, 2026
97a609a
v0.6.86: CORS updates, OAuth MCP, navigation pinning dynamic pages, g…
waleedlatif1 May 21, 2026
fde70e2
v0.6.87: performance improvements
icecrasher321 May 21, 2026
e9ee351
v0.6.88: mutex lock on oauth refresh, files export fix, hubspot trigg…
waleedlatif1 May 22, 2026
b5b2d83
v0.6.89: connectors ui, perf improvements, mcp hardening, og image
waleedlatif1 May 23, 2026
f6c9998
v0.6.90: resource breadcrumb flash fix, dedupe external URL fetches, …
icecrasher321 May 23, 2026
e532e0a
v0.6.91: file zoom, Zoom KB connector, error classifications, LiteLLM…
waleedlatif1 May 26, 2026
fd19470
v0.6.92: enrichment table column type, table run fixes, scheduled jit…
TheodoreSpeaks May 27, 2026
856182b
v0.6.93: schedules/mcp performance improvements, integration bugfixes
icecrasher321 May 28, 2026
6bf9e96
v0.6.94: 4.8 opus, better auth upgrade, zoominfo integration, copilot…
waleedlatif1 May 29, 2026
503432c
v0.6.95: data enrichment block, nullable workflow description fix
TheodoreSpeaks May 29, 2026
a8dcdd5
v0.6.96: pinned table columns, sequence number in copilot messages, t…
waleedlatif1 May 30, 2026
2f1f633
v0.6.97: migration fix for copilot_messages
icecrasher321 May 30, 2026
e32699d
v0.6.98: redundant index, security hardening, new copilot messages ta…
waleedlatif1 May 31, 2026
12ada0c
v0.6.99: tables filter operators, copilot chat persistence consolidat…
waleedlatif1 Jun 2, 2026
e8f09ae
v0.6.100: auth, mothership, scopes improvements, new apify tools
icecrasher321 Jun 2, 2026
3ba8668
v0.6.101: 11 new knowledgebase connectors, slack scopes update, login…
waleedlatif1 Jun 3, 2026
1192e20
v0.6.102: support S3-compatible in object storage, GitLab code knowle…
waleedlatif1 Jun 3, 2026
1ce8e92
v0.6.103: readme updates, tables lifecycle improvements, new connecto…
waleedlatif1 Jun 6, 2026
0c2df1e
v0.7.0: vibes improvement, new UI, new tools, chat-first, mothership …
waleedlatif1 Jun 10, 2026
7ffc495
v0.7.1: chat voice mode model update, sim trigger, codepipeline integ…
waleedlatif1 Jun 10, 2026
d4722f9
v0.7.2: logs export security, code hygiene, mship cost attribution
icecrasher321 Jun 10, 2026
f4d22ff
v0.7.3: jira oauth scope fix, read-replica client, table wire data fi…
TheodoreSpeaks Jun 11, 2026
a48b4a1
v0.7.4: round-robin byok support, table block fix, db read replica ro…
waleedlatif1 Jun 12, 2026
79d98b3
v0.7.5: deployments API and block, vanta integration, performance imp…
waleedlatif1 Jun 13, 2026
e6587ca
v0.7.6: calendar scheduled tasks, new hubspot tools, virtualized chat…
waleedlatif1 Jun 15, 2026
8c3706e
v0.7.7: square, context.dev integrations, scheduled tasks styling cha…
waleedlatif1 Jun 15, 2026
59d9496
v0.7.8: security hardening, code hygiene, MSFT oauth provider, new at…
waleedlatif1 Jun 16, 2026
56a88a2
v0.7.9: agent file attachments, chat autoscroll, knowledge base uploa…
icecrasher321 Jun 16, 2026
db47da5
v0.7.10: models sorting, compress/decompress file block tools, new en…
icecrasher321 Jun 18, 2026
8df34a3
v0.7.11: parallel subagents, new tools, rich markdown editor, governa…
waleedlatif1 Jun 21, 2026
aaca750
v0.7.12: mcp servers ui/ux fixes, nuqs for query param management
icecrasher321 Jun 22, 2026
ad0b867
v0.7.13: pii redaction, react query frontend refactor, pi coding agen…
TheodoreSpeaks Jun 24, 2026
11168f9
v0.7.14: perf improvements, code hygiene, GitLab private host support
waleedlatif1 Jun 25, 2026
613e8ea
v0.7.15: academy, perf improvements, db attribution, kb tags filter f…
waleedlatif1 Jun 27, 2026
38c088a
v0.7.16: security hardening, db o11y and profiling, settings UI
waleedlatif1 Jun 28, 2026
0371856
v0.7.17: emcn and workflow renderer isolated package, popular blocks …
waleedlatif1 Jun 30, 2026
3cc2692
fix(attachments): cross tenant security hardening
icecrasher321 Jul 1, 2026
135512e
address comments
icecrasher321 Jul 1, 2026
df8b98f
fix
icecrasher321 Jul 1, 2026
70c3395
fix
icecrasher321 Jul 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 7 additions & 11 deletions apps/sim/app/api/files/download/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { fileDownloadContract } from '@/lib/api/contracts/storage-transfer'
import { getValidationErrorMessage, parseRequest } from '@/lib/api/server'
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import type { StorageContext } from '@/lib/uploads/config'
import { hasCloudStorage } from '@/lib/uploads/core/storage-service'
import { inferContextFromKey } from '@/lib/uploads/utils/file-utils'
import { verifyFileAccess } from '@/app/api/files/authorization'
import { createErrorResponse, FileNotFoundError } from '@/app/api/files/utils'

Expand Down Expand Up @@ -40,7 +40,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
)
if (!parsed.success) return parsed.response

const { key, name, isExecutionFile, context, url } = parsed.data.body
const { key, name, url } = parsed.data.body

if (!key) {
return createErrorResponse(new Error('File key is required'), 400)
Expand All @@ -58,12 +58,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
})
}

let storageContext: StorageContext | 'general' | undefined = context

if (isExecutionFile && !context) {
storageContext = 'execution'
logger.info(`Using execution context for file: ${key}`)
}
// Derive context from the trusted key prefix, mirroring the serve route this URL
// delegates to, which re-derives context from the key and ignores any client-supplied value.
const storageContext = inferContextFromKey(key)
Comment thread
icecrasher321 marked this conversation as resolved.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Legacy downloads still fail

This endpoint still calls inferContextFromKey(key) directly, even though the request contract still accepts context and isExecutionFile for files whose private storage context cannot be inferred from the key. For a stored legacy file such as legacy/ws/wf/ex/report.pdf with context: 'execution', this throws before access verification or URL creation. The client gets a failed download response instead of a URL for a file that the new trusted-context helper is meant to keep readable.


const hasAccess = await verifyFileAccess(
key,
Expand All @@ -79,10 +76,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
}

const { getBaseUrl } = await import('@/lib/core/utils/urls')
const contextQuery = storageContext ? `?context=${storageContext}` : ''
const downloadUrl = `${getBaseUrl()}/api/files/serve/${encodeURIComponent(key)}${contextQuery}`
const downloadUrl = `${getBaseUrl()}/api/files/serve/${encodeURIComponent(key)}?context=${storageContext}`
Comment thread
icecrasher321 marked this conversation as resolved.

logger.info(`Generated download URL for ${storageContext ?? 'inferred'} file: ${key}`)
logger.info(`Generated download URL for ${storageContext} file: ${key}`)

return NextResponse.json({
downloadUrl,
Expand Down
9 changes: 3 additions & 6 deletions apps/sim/app/api/files/parse/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { checkInternalAuth } from '@/lib/auth/hybrid'
import { sanitizeUrlForLog } from '@/lib/core/utils/logging'
import { assertKnownSizeWithinLimit, isPayloadSizeLimitError } from '@/lib/core/utils/stream-limits'
import { isSupportedFileType, parseFile } from '@/lib/file-parsers'
import { isUsingCloudStorage, type StorageContext, StorageService } from '@/lib/uploads'
import { isUsingCloudStorage, StorageService } from '@/lib/uploads'
import { uploadExecutionFile } from '@/lib/uploads/contexts/execution'
import {
ExternalUrlValidationError,
Expand Down Expand Up @@ -303,7 +303,6 @@ async function parseFileSingle(
return handleCloudFile(
filePath,
fileType,
undefined,
userId,
executionContext,
maxDownloadBytes,
Expand All @@ -329,7 +328,6 @@ async function parseFileSingle(
return handleCloudFile(
filePath,
fileType,
undefined,
userId,
executionContext,
maxDownloadBytes,
Expand Down Expand Up @@ -608,7 +606,6 @@ async function handleExternalUrl(
async function handleCloudFile(
filePath: string,
fileType: string,
explicitContext: string | undefined,
userId: string,
executionContext?: ExecutionContext,
maxDownloadBytes = MAX_DOWNLOAD_SIZE_BYTES,
Expand All @@ -619,7 +616,7 @@ async function handleCloudFile(

logger.info('Extracted cloud key:', cloudKey)

const context = (explicitContext as StorageContext) || inferContextFromKey(cloudKey)
const context = inferContextFromKey(cloudKey)

const hasAccess = await verifyFileAccess(
cloudKey,
Expand Down Expand Up @@ -658,7 +655,7 @@ async function handleCloudFile(
maxBytes: maxDownloadBytes,
})
logger.info(
`Downloaded file from ${context} storage (${explicitContext ? 'explicit' : 'inferred'}): ${cloudKey}, size: ${fileBuffer.length} bytes`
`Downloaded file from ${context} storage: ${cloudKey}, size: ${fileBuffer.length} bytes`
)

const filename = originalFilename || cloudKey.split('/').pop() || cloudKey
Expand Down
47 changes: 47 additions & 0 deletions apps/sim/lib/uploads/utils/file-utils.server.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* @vitest-environment node
*/
import { beforeEach, describe, expect, it, vi } from 'vitest'

const { mockDownloadFile } = vi.hoisted(() => ({
mockDownloadFile: vi.fn(),
}))

vi.mock('@/lib/uploads/core/storage-service', () => ({
downloadFile: mockDownloadFile,
hasCloudStorage: vi.fn(() => true),
}))

vi.mock('@/app/api/files/authorization', () => ({
verifyFileAccess: vi.fn(),
}))

import { createLogger } from '@sim/logger'
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
import type { UserFile } from '@/executor/types'

describe('downloadFileFromStorage context derivation', () => {
beforeEach(() => {
vi.clearAllMocks()
mockDownloadFile.mockResolvedValue(Buffer.from('bytes'))
})

it('downloads with the key-derived context, ignoring a caller-supplied public context', async () => {
const userFile: UserFile = {
id: 'f1',
name: 'report.pdf',
url: '',
size: 5,
type: 'application/pdf',
key: 'workspace/ws-1/1700000000000-abc1234-report.pdf',
context: 'og-images',
}

await downloadFileFromStorage(userFile, 'req-1', createLogger('test'))

expect(mockDownloadFile).toHaveBeenCalledTimes(1)
expect(mockDownloadFile).toHaveBeenCalledWith(
expect.objectContaining({ key: userFile.key, context: 'workspace' })
)
})
})
10 changes: 4 additions & 6 deletions apps/sim/lib/uploads/utils/file-utils.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import {
consumeOrCancelBody,
readResponseToBufferWithLimit,
} from '@/lib/core/utils/stream-limits'
import type { StorageContext } from '@/lib/uploads'
import { StorageService } from '@/lib/uploads'
import { isExecutionFile } from '@/lib/uploads/contexts/execution/utils'
import {
Expand All @@ -23,6 +22,7 @@ import {
isInternalFileUrl,
processSingleFileToUserFile,
type RawFileInput,
resolveTrustedFileContext,
} from '@/lib/uploads/utils/file-utils'
import { verifyFileAccess } from '@/app/api/files/authorization'
import type { UserFile } from '@/executor/types'
Expand Down Expand Up @@ -90,7 +90,7 @@ export async function resolveFileInputToUrl(

// Generate presigned URL if we have a key but no URL
if (!fileUrl && userFile.key) {
const context = (userFile.context as StorageContext) || inferContextFromKey(userFile.key)
const context = resolveTrustedFileContext(userFile.key, userFile.context)
const hasAccess = await verifyFileAccess(userFile.key, userId, undefined, context, false)

if (!hasAccess) {
Expand Down Expand Up @@ -281,10 +281,8 @@ export async function downloadFileFromStorage(
)
buffer = await downloadExecutionFile(userFile, { maxBytes: options.maxBytes })
} else if (userFile.key) {
const context = (userFile.context as StorageContext) || inferContextFromKey(userFile.key)
logger.info(
`[${requestId}] Downloading from ${context} storage (${userFile.context ? 'explicit' : 'inferred'}): ${userFile.key}`
)
const context = resolveTrustedFileContext(userFile.key, userFile.context)
logger.info(`[${requestId}] Downloading from ${context} storage: ${userFile.key}`)

const { downloadFile } = await import('@/lib/uploads/core/storage-service')
buffer = await downloadFile({
Expand Down
21 changes: 21 additions & 0 deletions apps/sim/lib/uploads/utils/file-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
isInternalFileUrl,
isNetworkError,
processSingleFileToUserFile,
resolveTrustedFileContext,
} from '@/lib/uploads/utils/file-utils'

const logger = createLogger('FileUtilsTest')
Expand Down Expand Up @@ -74,6 +75,26 @@ describe('inferContextFromKey', () => {
})
})

describe('resolveTrustedFileContext', () => {
it('derives from the key prefix and ignores a mismatched caller context', () => {
expect(resolveTrustedFileContext('workspace/ws/1700000000000-abc-x.pdf', 'og-images')).toBe(
'workspace'
)
expect(resolveTrustedFileContext('chat/x', 'workspace-logos')).toBe('chat')
expect(resolveTrustedFileContext('workspace/ws/x', 'mothership')).toBe('workspace')
})

it('honors the caller context for legacy keys with no inferrable prefix', () => {
expect(resolveTrustedFileContext('legacy/ws/wf/ex/report.pdf', 'execution')).toBe('execution')
})

it('never resolves an un-inferrable key to a world-readable context', () => {
expect(() => resolveTrustedFileContext('legacy/report.pdf', 'og-images')).toThrow()
expect(() => resolveTrustedFileContext('legacy/report.pdf', 'profile-pictures')).toThrow()
expect(() => resolveTrustedFileContext('legacy/report.pdf')).toThrow()
})
})

describe('isAbortError', () => {
it('returns true for AbortError-named errors', () => {
const err = new Error('aborted')
Expand Down
37 changes: 37 additions & 0 deletions apps/sim/lib/uploads/utils/file-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,43 @@ export function inferContextFromKey(key: string): StorageContext {
)
}

/**
* World-readable storage contexts. Reads for these short-circuit file
* authorization and can resolve to the shared bucket, so a caller-supplied
* context must never select one for a key that does not carry the matching
* prefix.
*/
const PUBLIC_STORAGE_CONTEXTS = new Set<StorageContext>([
'profile-pictures',
'og-images',
'workspace-logos',
])

/**
* Resolve the storage context for a stored file from its trusted key prefix.
*
* The storage key is written server-side at upload time and cannot be forged to
* change tenant, whereas a file's `context` field is attacker-authorable in a
* workflow. When the key carries a recognized prefix that prefix is
* authoritative and the caller-supplied `context` is ignored — this prevents a
* private `workspace/…` key from being relabeled with a world-readable context
* to bypass authorization and read the shared bucket.
*
* Legacy keys predating context-prefixed keys cannot be inferred; for those the
* persisted `context` is honored so existing files stay resolvable — except a
* world-readable context, which would reopen the bypass on an un-inferrable key.
*/
export function resolveTrustedFileContext(key: string, context?: string): StorageContext {
try {
return inferContextFromKey(key)
} catch (error) {
if (context && !PUBLIC_STORAGE_CONTEXTS.has(context as StorageContext)) {
return context as StorageContext
}
throw error
}
}

/**
* Extract storage key and context from an internal file URL
* @param fileUrl - Internal file URL (e.g., /api/files/serve/key?context=workspace)
Expand Down
7 changes: 3 additions & 4 deletions apps/sim/providers/file-attachments.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@ import { FileState, GoogleGenAI } from '@google/genai'
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { sleep } from '@sim/utils/helpers'
import type { StorageContext } from '@/lib/uploads'
import { StorageService } from '@/lib/uploads'
import { inferContextFromKey } from '@/lib/uploads/utils/file-utils'
import { resolveTrustedFileContext } from '@/lib/uploads/utils/file-utils'
import { downloadServableFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
import { verifyFileAccess } from '@/app/api/files/authorization'
import type { UserFile } from '@/executor/types'
Expand Down Expand Up @@ -85,7 +84,7 @@ export async function attachLargeFileRemoteUrls(
)
}

const context = (file.context as StorageContext) || inferContextFromKey(file.key)
const context = resolveTrustedFileContext(file.key, file.context)
const hasAccess = await verifyFileAccess(file.key, request.userId, undefined, context, false)
if (!hasAccess) {
throw new Error(`File "${file.name}" is not accessible for provider "${providerId}"`)
Expand Down Expand Up @@ -147,7 +146,7 @@ async function assertFileAccessForUpload(
if (!userId) {
throw new Error(`File "${file.name}" requires an authenticated user to upload`)
}
const context = (file.context as StorageContext) || inferContextFromKey(file.key)
const context = resolveTrustedFileContext(file.key, file.context)
const hasAccess = await verifyFileAccess(file.key, userId, undefined, context, false)
if (!hasAccess) {
throw new Error(`File "${file.name}" is not accessible`)
Expand Down
Loading