Skip to content

Commit 3424a33

Browse files
authored
fix(security): fixed SSRF vulnerability (#1149)
1 parent 51b1e97 commit 3424a33

File tree

4 files changed

+942
-14
lines changed

4 files changed

+942
-14
lines changed

apps/sim/app/api/proxy/image/route.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { type NextRequest, NextResponse } from 'next/server'
22
import { createLogger } from '@/lib/logs/console/logger'
3+
import { validateImageUrl } from '@/lib/security/url-validation'
34

45
const logger = createLogger('ImageProxyAPI')
56

@@ -17,10 +18,18 @@ export async function GET(request: NextRequest) {
1718
return new NextResponse('Missing URL parameter', { status: 400 })
1819
}
1920

21+
const urlValidation = validateImageUrl(imageUrl)
22+
if (!urlValidation.isValid) {
23+
logger.warn(`[${requestId}] Blocked image proxy request`, {
24+
url: imageUrl.substring(0, 100),
25+
error: urlValidation.error,
26+
})
27+
return new NextResponse(urlValidation.error || 'Invalid image URL', { status: 403 })
28+
}
29+
2030
logger.info(`[${requestId}] Proxying image request for: ${imageUrl}`)
2131

2232
try {
23-
// Use fetch with custom headers that appear more browser-like
2433
const imageResponse = await fetch(imageUrl, {
2534
headers: {
2635
'User-Agent':
@@ -45,18 +54,15 @@ export async function GET(request: NextRequest) {
4554
})
4655
}
4756

48-
// Get image content type from response headers
4957
const contentType = imageResponse.headers.get('content-type') || 'image/jpeg'
5058

51-
// Get the image as a blob
5259
const imageBlob = await imageResponse.blob()
5360

5461
if (imageBlob.size === 0) {
5562
logger.error(`[${requestId}] Empty image blob received`)
5663
return new NextResponse('Empty image received', { status: 404 })
5764
}
5865

59-
// Return the image with appropriate headers
6066
return new NextResponse(imageBlob, {
6167
headers: {
6268
'Content-Type': contentType,

apps/sim/app/api/proxy/route.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { NextResponse } from 'next/server'
22
import { isDev } from '@/lib/environment'
33
import { createLogger } from '@/lib/logs/console/logger'
4+
import { validateProxyUrl } from '@/lib/security/url-validation'
45
import { executeTool } from '@/tools'
56
import { getTool, validateRequiredParametersAfterMerge } from '@/tools/utils'
67

@@ -80,6 +81,15 @@ export async function GET(request: Request) {
8081
return createErrorResponse("Missing 'url' parameter", 400)
8182
}
8283

84+
const urlValidation = validateProxyUrl(targetUrl)
85+
if (!urlValidation.isValid) {
86+
logger.warn(`[${requestId}] Blocked proxy request`, {
87+
url: targetUrl.substring(0, 100),
88+
error: urlValidation.error,
89+
})
90+
return createErrorResponse(urlValidation.error || 'Invalid URL', 403)
91+
}
92+
8393
const method = url.searchParams.get('method') || 'GET'
8494

8595
const bodyParam = url.searchParams.get('body')
@@ -109,7 +119,6 @@ export async function GET(request: Request) {
109119
logger.info(`[${requestId}] Proxying ${method} request to: ${targetUrl}`)
110120

111121
try {
112-
// Forward the request to the target URL with all specified headers
113122
const response = await fetch(targetUrl, {
114123
method: method,
115124
headers: {
@@ -119,7 +128,6 @@ export async function GET(request: Request) {
119128
body: body || undefined,
120129
})
121130

122-
// Get response data
123131
const contentType = response.headers.get('content-type') || ''
124132
let data
125133

@@ -129,7 +137,6 @@ export async function GET(request: Request) {
129137
data = await response.text()
130138
}
131139

132-
// For error responses, include a more descriptive error message
133140
const errorMessage = !response.ok
134141
? data && typeof data === 'object' && data.error
135142
? `${data.error.message || JSON.stringify(data.error)}`
@@ -140,7 +147,6 @@ export async function GET(request: Request) {
140147
logger.error(`[${requestId}] External API error: ${response.status} ${response.statusText}`)
141148
}
142149

143-
// Return the proxied response
144150
return formatResponse({
145151
success: response.ok,
146152
status: response.status,
@@ -166,7 +172,6 @@ export async function POST(request: Request) {
166172
const startTimeISO = startTime.toISOString()
167173

168174
try {
169-
// Parse request body
170175
let requestBody
171176
try {
172177
requestBody = await request.json()
@@ -186,23 +191,20 @@ export async function POST(request: Request) {
186191

187192
logger.info(`[${requestId}] Processing tool: ${toolId}`)
188193

189-
// Get tool
190194
const tool = getTool(toolId)
191195

192196
if (!tool) {
193197
logger.error(`[${requestId}] Tool not found: ${toolId}`)
194198
throw new Error(`Tool not found: ${toolId}`)
195199
}
196200

197-
// Validate the tool and its parameters
198201
try {
199202
validateRequiredParametersAfterMerge(toolId, tool, params)
200203
} catch (validationError) {
201204
logger.warn(`[${requestId}] Tool validation failed for ${toolId}`, {
202205
error: validationError instanceof Error ? validationError.message : String(validationError),
203206
})
204207

205-
// Add timing information even to error responses
206208
const endTime = new Date()
207209
const endTimeISO = endTime.toISOString()
208210
const duration = endTime.getTime() - startTime.getTime()
@@ -214,14 +216,12 @@ export async function POST(request: Request) {
214216
})
215217
}
216218

217-
// Check if tool has file outputs - if so, don't skip post-processing
218219
const hasFileOutputs =
219220
tool.outputs &&
220221
Object.values(tool.outputs).some(
221222
(output) => output.type === 'file' || output.type === 'file[]'
222223
)
223224

224-
// Execute tool
225225
const result = await executeTool(
226226
toolId,
227227
params,

0 commit comments

Comments
 (0)