Skip to content

Commit 660bd58

Browse files
committed
fix(supabase): report storage path-guard failures as 400, not 500
cubic found that adding throwing guards to encodeStoragePath and encodeStorageSegment changed how caller mistakes are attributed. Nothing in those helpers threw before this branch, so the new rejections fall through to the generic handler in executeSupabaseStorageUpload and surface as HTTP 500 — blaming the server for a caller's '..' — while validateSupabaseProjectId one line above already reports a bad project id as 400. Maps guard rejections to 400 at both call sites, keeping the guard's named message. storage-get-public-url surfaces the same rejection through its own result shape rather than throwing out of the operation. Pinned with tests over traversal, empty-segment and bad-bucket inputs asserting 400 and that no provider request is made. Verified non-vacuous: removing the mapping fails all four.
1 parent 56ac387 commit 660bd58

3 files changed

Lines changed: 77 additions & 4 deletions

File tree

‎apps/sim/lib/internal/supabase/operations.test.ts‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,3 +124,44 @@ describe('executeSupabaseStorageUpload', () => {
124124
).rejects.toMatchObject({ name: 'AbortError' })
125125
})
126126
})
127+
128+
/**
129+
* The storage path guards throw a plain `Error` on caller-supplied values.
130+
* Before they existed nothing here threw, so an unmapped throw would surface as
131+
* HTTP 500 and blame the server for the caller's `..` — while
132+
* `validateSupabaseProjectId` one line above already reports a bad project id
133+
* as 400. These assertions pin the attribution and the named message.
134+
*/
135+
describe('executeSupabaseStorageUpload path-guard failures', () => {
136+
beforeEach(() => {
137+
vi.clearAllMocks()
138+
mocks.assertToolFileAccess.mockResolvedValue(null)
139+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(Response.json({ Key: 'k' })))
140+
})
141+
142+
it.each([
143+
['a traversal path', { path: '../..', fileName: 'x.txt' }],
144+
['an empty path segment', { path: 'a//b', fileName: 'x.txt' }],
145+
['a bucket that is a dot segment', { bucket: '..', fileName: 'x.txt' }],
146+
['a bucket carrying a separator', { bucket: 'a/b', fileName: 'x.txt' }],
147+
])('reports %s as 400, not 500', async (_label, overrides) => {
148+
const response = await executeSupabaseStorageUpload(
149+
{ ...BASE_INPUT, fileData: 'hello', ...overrides },
150+
{ userId: 'user-1', requestId: 'request-1' }
151+
)
152+
153+
expect(response.status).toBe(400)
154+
await expect(response.json()).resolves.toMatchObject({ success: false })
155+
expect(fetch).not.toHaveBeenCalled()
156+
})
157+
158+
it('names the offending parameter in the error', async () => {
159+
const response = await executeSupabaseStorageUpload(
160+
{ ...BASE_INPUT, bucket: '..', fileData: 'hello' },
161+
{ userId: 'user-1', requestId: 'request-1' }
162+
)
163+
const body = (await response.json()) as { error?: string }
164+
165+
expect(body.error).toMatch(/bucket/)
166+
})
167+
})

‎apps/sim/lib/internal/supabase/operations.ts‎

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,23 @@ export async function executeSupabaseStorageUpload(
120120
const fullPath = input.path
121121
? `${input.path.endsWith('/') ? input.path : `${input.path}/`}${input.fileName}`
122122
: input.fileName
123-
const encodedBucket = encodeStorageSegment(input.bucket)
124-
const encodedPath = encodeStoragePath(fullPath)
123+
/**
124+
* The storage path guards throw a plain `Error`, and the catch at the end of
125+
* this function maps anything that is not a payload-size failure to 500.
126+
* Every value they reject is caller-supplied — a `bucket` of `..`, a `path`
127+
* with an empty segment — so reporting it as a server fault both
128+
* misattributes the blame and buries the guard's named message behind a
129+
* generic status. 400 is the accurate answer, and it matches how
130+
* `validateSupabaseProjectId` above already reports a bad project id.
131+
*/
132+
let encodedBucket: string
133+
let encodedPath: string
134+
try {
135+
encodedBucket = encodeStorageSegment(input.bucket)
136+
encodedPath = encodeStoragePath(fullPath)
137+
} catch (error) {
138+
return failureResponse(getErrorMessage(error, 'Invalid storage path'), 400)
139+
}
125140
const baseUrl = `https://${projectValidation.sanitized}.supabase.co/storage/v1/object`
126141
const headers: Record<string, string> = {
127142
apikey: input.apiKey,

‎apps/sim/lib/internal/supabase/operations/storage-get-public-url.ts‎

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,29 @@
1+
import { getErrorMessage } from '@sim/utils/errors'
12
import type { InternalToolOperationImplementation } from '@/lib/internal/tool-operations/types'
23
import type { SupabaseStorageGetPublicUrlParams } from '@/tools/supabase/types'
34
import { encodeStoragePath, encodeStorageSegment, supabaseBaseUrl } from '@/tools/supabase/utils'
45

56
export const executeStorageGetPublicUrlOperation: InternalToolOperationImplementation<
67
SupabaseStorageGetPublicUrlParams
78
> = async (params: SupabaseStorageGetPublicUrlParams) => {
8-
const bucket = encodeStorageSegment(params.bucket)
9-
const path = encodeStoragePath(params.path)
9+
/**
10+
* Same reasoning as the upload operation: the path guards throw on
11+
* caller-supplied values, and an uncaught throw here escapes as an opaque
12+
* server failure rather than the guard's named message. This operation
13+
* reports failure in its own result shape, so the rejection is surfaced there.
14+
*/
15+
let bucket: string
16+
let path: string
17+
try {
18+
bucket = encodeStorageSegment(params.bucket)
19+
path = encodeStoragePath(params.path)
20+
} catch (error) {
21+
return {
22+
success: false,
23+
output: { message: getErrorMessage(error, 'Invalid storage path'), publicUrl: '' },
24+
error: getErrorMessage(error, 'Invalid storage path'),
25+
}
26+
}
1027
let publicUrl = `${supabaseBaseUrl(params.projectId)}/storage/v1/object/public/${bucket}/${path}`
1128

1229
if (params.download) {

0 commit comments

Comments
 (0)