Skip to content

Commit 118a8f0

Browse files
committed
fix(security): supabase rpc path validation, ssh stream byte cap, storage quota coverage
1 parent 80c9a01 commit 118a8f0

5 files changed

Lines changed: 41 additions & 15 deletions

File tree

apps/sim/app/api/files/multipart/route.ts

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import {
2222
type UploadTokenPayload,
2323
verifyUploadToken,
2424
} from '@/lib/uploads/core/upload-token'
25-
import type { StorageConfig } from '@/lib/uploads/shared/types'
25+
import { QUOTA_EXEMPT_STORAGE_CONTEXTS, type StorageConfig } from '@/lib/uploads/shared/types'
2626
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
2727

2828
const logger = createLogger('MultipartUploadAPI')
@@ -135,6 +135,22 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
135135

136136
const config = getStorageConfig(storageContext)
137137

138+
// Apply storage quota to all user-driven upload contexts (not system/metadata contexts)
139+
if (
140+
!QUOTA_EXEMPT_STORAGE_CONTEXTS.has(context as StorageContext) &&
141+
typeof fileSize === 'number' &&
142+
fileSize > 0
143+
) {
144+
const { checkStorageQuota } = await import('@/lib/billing/storage')
145+
const quotaCheck = await checkStorageQuota(userId, fileSize)
146+
if (!quotaCheck.allowed) {
147+
return NextResponse.json(
148+
{ error: quotaCheck.error || 'Storage limit exceeded' },
149+
{ status: 413 }
150+
)
151+
}
152+
}
153+
138154
let customKey: string | undefined
139155
if (context === 'workspace' || context === 'mothership') {
140156
const { MAX_WORKSPACE_FILE_SIZE } = await import('@/lib/uploads/shared/types')
@@ -149,15 +165,6 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
149165
'@/lib/uploads/contexts/workspace/workspace-file-manager'
150166
)
151167
customKey = generateWorkspaceFileKey(workspaceId, fileName)
152-
153-
const { checkStorageQuota } = await import('@/lib/billing/storage')
154-
const quotaCheck = await checkStorageQuota(userId, fileSize)
155-
if (!quotaCheck.allowed) {
156-
return NextResponse.json(
157-
{ error: quotaCheck.error || 'Storage limit exceeded' },
158-
{ status: 413 }
159-
)
160-
}
161168
} else if (context === 'execution') {
162169
const workflowId = (data as { workflowId?: unknown }).workflowId
163170
const executionId = (data as { executionId?: unknown }).executionId

apps/sim/app/api/tools/ssh/read-file-content/route.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,16 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
7373

7474
const content = await new Promise<string>((resolve, reject) => {
7575
const chunks: Buffer[] = []
76+
let totalBytes = 0
7677
const readStream = sftp.createReadStream(filePath)
7778

7879
readStream.on('data', (chunk: Buffer) => {
80+
totalBytes += chunk.length
81+
if (totalBytes > maxBytes) {
82+
readStream.destroy()
83+
reject(new Error(`File exceeds maximum allowed size of ${params.maxSize}MB`))
84+
return
85+
}
7986
chunks.push(chunk)
8087
})
8188

apps/sim/lib/uploads/shared/types.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,17 @@ export type StorageContext =
2323
| 'logs'
2424
| 'workspace-logos'
2525

26+
/**
27+
* Contexts exempt from storage quota checks — small metadata assets not managed
28+
* by the user (profile pictures, logos, OG images). All other contexts represent
29+
* user-driven uploads and must pass quota validation before upload is initiated.
30+
*/
31+
export const QUOTA_EXEMPT_STORAGE_CONTEXTS = new Set<StorageContext>([
32+
'profile-pictures',
33+
'workspace-logos',
34+
'og-images',
35+
])
36+
2637
export interface FileInfo {
2738
path: string
2839
key: string

apps/sim/tools/supabase/vector_search.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { validateDatabaseIdentifier } from '@/lib/core/security/input-validation'
12
import type {
23
SupabaseVectorSearchParams,
34
SupabaseVectorSearchResponse,
@@ -56,8 +57,9 @@ export const vectorSearchTool: ToolConfig<
5657

5758
request: {
5859
url: (params) => {
59-
// Use RPC endpoint for calling PostgreSQL functions
60-
return `${supabaseBaseUrl(params.projectId)}/rest/v1/rpc/${params.functionName}`
60+
const fnValidation = validateDatabaseIdentifier(params.functionName, 'functionName')
61+
if (!fnValidation.isValid) throw new Error(fnValidation.error)
62+
return `${supabaseBaseUrl(params.projectId)}/rest/v1/rpc/${encodeURIComponent(params.functionName)}`
6163
},
6264
method: 'POST',
6365
headers: (params) => ({

biome.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"includes": [
77
"**",
88
"!**/.next",
9-
"!**/.next/**",
9+
"!**/.next",
1010
"!**/next-env.d.ts",
1111
"!**/out",
1212
"!**/dist",
@@ -69,8 +69,7 @@
6969
"rules": {
7070
"recommended": true,
7171
"nursery": {
72-
"useSortedClasses": "warn",
73-
"noNestedComponentDefinitions": "off"
72+
"useSortedClasses": "warn"
7473
},
7574
"a11y": {
7675
"noSvgWithoutTitle": "off",

0 commit comments

Comments
 (0)