-
Notifications
You must be signed in to change notification settings - Fork 3.7k
fix(attachments): cross tenant hardening #5310
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: staging
Are you sure you want to change the base?
Changes from all commits
0b9019d
a54dcbe
28af223
d889f32
316bc8c
3f508e4
d6ec115
d7da35b
cf233bb
f8f3758
3c8bb40
d33acf4
4f40c4c
cbfab1c
4309d06
8b57476
e3d0e74
0ac0539
3838b6e
fc07922
3a1b1a8
46ffc49
010435c
c0bc62c
387cc97
2dbc7fd
8a50f18
dcf3302
bc09865
5f56e46
ca3bbf1
bbf400f
7c619e7
64cfda5
7ca736a
6066fc1
3422f64
595c4c3
d6c1bc2
58a3ae2
489f2d3
6aa3fe3
ecbf5e5
2aaf2b7
d445b9c
4bc6a17
5be12f8
4253e57
8d6b615
efcd51a
8d934f3
5ea80a8
3cc581e
273e608
07b8f1b
dcaf3e9
6aeb981
3e9849b
64d855a
ab156b5
c09a2c9
6a5eebc
4efe999
f69a9a0
db7f1c1
dbe8e51
11bcb8f
d14af04
e6b3cce
97a609a
fde70e2
e9ee351
b5b2d83
f6c9998
e532e0a
fd19470
856182b
6bf9e96
503432c
a8dcdd5
2f1f633
e32699d
12ada0c
e8f09ae
3ba8668
1192e20
1ce8e92
0c2df1e
7ffc495
d4722f9
f4d22ff
a48b4a1
79d98b3
e6587ca
8c3706e
59d9496
56a88a2
db47da5
8df34a3
aaca750
ad0b867
11168f9
613e8ea
38c088a
0371856
3cc2692
135512e
df8b98f
70c3395
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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' | ||
|
|
||
|
|
@@ -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) | ||
|
|
@@ -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) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This endpoint still calls |
||
|
|
||
| const hasAccess = await verifyFileAccess( | ||
| key, | ||
|
|
@@ -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}` | ||
|
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, | ||
|
|
||
| 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' }) | ||
| ) | ||
| }) | ||
| }) |
Uh oh!
There was an error while loading. Please reload this page.