Skip to content

Commit e9b2e11

Browse files
committed
fix(uploads): derive internal file context from key, not query param
Cursor Bugbot flagged a context-spoofing bypass: downloadFileFromUrl resolved context via parseInternalFileUrl, which honors a caller-controlled ?context= query param. An attacker could label a private storage key with a world-readable context (profile-pictures/og-images/workspace-logos) so verifyFileAccess short-circuits to granted while downloadFile still reads the private object. Infer context from the key only (inferContextFromKey), mirroring how /api/files/serve resolves it; ignore the query param. Also move the userId guard ahead of key resolution so auth failures surface first.
1 parent 41ae711 commit e9b2e11

1 file changed

Lines changed: 14 additions & 4 deletions

File tree

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

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -168,16 +168,26 @@ export async function downloadFileFromUrl(
168168
options: DownloadFileFromUrlOptions = {}
169169
): Promise<Buffer> {
170170
const { timeoutMs = getMaxExecutionTimeout(), maxBytes, userId } = options
171-
const { parseInternalFileUrl } = await import('./file-utils')
172171

173172
if (isInternalFileUrl(fileUrl)) {
174-
const { key, context } = parseInternalFileUrl(fileUrl)
175-
176173
if (!userId) {
177-
logger.warn('Internal file download denied: no userId provided', { key, context })
174+
logger.warn('Internal file download denied: no userId provided', { fileUrl })
178175
throw new Error('Access denied: internal file URL requires an authenticated user')
179176
}
180177

178+
const key = extractStorageKey(fileUrl)
179+
if (!key) {
180+
logger.warn('Internal file download denied: could not resolve storage key', { fileUrl })
181+
throw new Error('Access denied: could not resolve internal file key')
182+
}
183+
184+
// Derive the storage context from the key itself, never from a caller-controlled
185+
// `?context=` query param. Trusting the param lets a private key be labeled with a
186+
// world-readable context (e.g. profile-pictures), so verifyFileAccess short-circuits
187+
// to granted while downloadFile still reads the private object. This mirrors how
188+
// /api/files/serve resolves context (inferContextFromKey only).
189+
const context = inferContextFromKey(key)
190+
181191
const hasAccess = await verifyFileAccess(key, userId, undefined, context, false)
182192
if (!hasAccess) {
183193
logger.warn('Internal file download denied: access check failed', { key, context, userId })

0 commit comments

Comments
 (0)