Skip to content

Commit 9eda861

Browse files
committed
refactor(providers): read files-api upload bytes via storage SDK
Read OpenAI/Gemini upload bytes through downloadFileFromStorage instead of HTTP-fetching the presigned URL. Removes any server-side URL fetch (no SSRF vector) and works with internal object storage (e.g. self-hosted MinIO), which an IP-pinned URL fetch would have blocked.
1 parent 48f79a4 commit 9eda861

1 file changed

Lines changed: 8 additions & 36 deletions

File tree

apps/sim/providers/file-attachments.server.ts

Lines changed: 8 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,10 @@ import { createLogger } from '@sim/logger'
33
import { getErrorMessage } from '@sim/utils/errors'
44
import { sleep } from '@sim/utils/helpers'
55
import OpenAI, { toFile } from 'openai'
6-
import {
7-
secureFetchWithPinnedIP,
8-
validateUrlWithDNS,
9-
} from '@/lib/core/security/input-validation.server'
10-
import { readResponseToBufferWithLimit } from '@/lib/core/utils/stream-limits'
116
import type { StorageContext } from '@/lib/uploads'
127
import { StorageService } from '@/lib/uploads'
138
import { inferContextFromKey } from '@/lib/uploads/utils/file-utils'
9+
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
1410
import { verifyFileAccess } from '@/app/api/files/authorization'
1511
import type { UserFile } from '@/executor/types'
1612
import {
@@ -148,36 +144,12 @@ function groupUploadableFiles(messages: Message[] | undefined): UserFile[][] {
148144
}
149145

150146
/**
151-
* Downloads the file from its signed URL with DNS validation and IP pinning so a URL that
152-
* somehow resolves to an internal address can never be fetched (SSRF defense for every
153-
* caller, not just the agent path). Bounded by the provider's attachment ceiling.
147+
* Reads the file bytes straight from storage via the storage SDK (not by HTTP-fetching the
148+
* signed URL), so there is no server-side URL fetch to be an SSRF vector and internal
149+
* object storage works. Bounded by the provider's attachment ceiling.
154150
*/
155-
async function fetchRemoteFileBlob(
156-
file: UserFile,
157-
maxBytes: number,
158-
signal?: AbortSignal
159-
): Promise<Blob> {
160-
const url = file.remoteUrl as string
161-
const validation = await validateUrlWithDNS(url, 'fileUrl')
162-
if (!validation.isValid || !validation.resolvedIP) {
163-
throw new Error(
164-
`Cannot download "${file.name}" for upload: ${validation.error || 'invalid URL'}`
165-
)
166-
}
167-
168-
const response = await secureFetchWithPinnedIP(url, validation.resolvedIP, {
169-
maxResponseBytes: maxBytes,
170-
signal,
171-
})
172-
if (!response.ok) {
173-
throw new Error(`Failed to download "${file.name}" for upload (status ${response.status})`)
174-
}
175-
176-
const buffer = await readResponseToBufferWithLimit(response, {
177-
maxBytes,
178-
label: 'provider file upload',
179-
signal,
180-
})
151+
async function downloadFileForUpload(file: UserFile, maxBytes: number): Promise<Blob> {
152+
const buffer = await downloadFileFromStorage(file, 'provider-file-upload', logger, { maxBytes })
181153
return new Blob([buffer], { type: file.type || inferAttachmentMimeType(file) })
182154
}
183155

@@ -188,7 +160,7 @@ async function uploadOpenAIFile(
188160
signal?: AbortSignal
189161
): Promise<void> {
190162
const mimeType = inferAttachmentMimeType(file)
191-
const blob = await fetchRemoteFileBlob(file, maxBytes, signal)
163+
const blob = await downloadFileForUpload(file, maxBytes)
192164

193165
const uploaded = await client.files.create(
194166
{
@@ -210,7 +182,7 @@ async function uploadGeminiFile(
210182
signal?: AbortSignal
211183
): Promise<void> {
212184
const mimeType = inferAttachmentMimeType(file)
213-
const blob = await fetchRemoteFileBlob(file, maxBytes, signal)
185+
const blob = await downloadFileForUpload(file, maxBytes)
214186

215187
let uploaded = await ai.files.upload({ file: blob, config: { mimeType, abortSignal: signal } })
216188

0 commit comments

Comments
 (0)