Skip to content

Commit eefbf53

Browse files
committed
fix(security): add authentication and input validation to API routes
1 parent 7f4edc8 commit eefbf53

File tree

49 files changed

+503
-43
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+503
-43
lines changed

apps/sim/app/api/a2a/agents/[agentId]/route.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type { AgentCapabilities, AgentSkill } from '@/lib/a2a/types'
88
import { checkHybridAuth } from '@/lib/auth/hybrid'
99
import { getRedisClient } from '@/lib/core/config/redis'
1010
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
11+
import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
1112

1213
const logger = createLogger('A2AAgentCardAPI')
1314

@@ -95,6 +96,11 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<Ro
9596
return NextResponse.json({ error: 'Agent not found' }, { status: 404 })
9697
}
9798

99+
const workspaceAccess = await checkWorkspaceAccess(existingAgent.workspaceId, auth.userId)
100+
if (!workspaceAccess.canWrite) {
101+
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
102+
}
103+
98104
const body = await request.json()
99105

100106
if (
@@ -160,6 +166,11 @@ export async function DELETE(request: NextRequest, { params }: { params: Promise
160166
return NextResponse.json({ error: 'Agent not found' }, { status: 404 })
161167
}
162168

169+
const workspaceAccess = await checkWorkspaceAccess(existingAgent.workspaceId, auth.userId)
170+
if (!workspaceAccess.canWrite) {
171+
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
172+
}
173+
163174
await db.delete(a2aAgent).where(eq(a2aAgent.id, agentId))
164175

165176
logger.info(`Deleted A2A agent: ${agentId}`)
@@ -194,6 +205,11 @@ export async function POST(request: NextRequest, { params }: { params: Promise<R
194205
return NextResponse.json({ error: 'Agent not found' }, { status: 404 })
195206
}
196207

208+
const workspaceAccess = await checkWorkspaceAccess(existingAgent.workspaceId, auth.userId)
209+
if (!workspaceAccess.canWrite) {
210+
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
211+
}
212+
197213
const body = await request.json()
198214
const action = body.action as 'publish' | 'unpublish' | 'refresh'
199215

apps/sim/app/api/a2a/serve/[agentId]/route.ts

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
import { checkHybridAuth } from '@/lib/auth/hybrid'
1717
import { getBrandConfig } from '@/lib/branding/branding'
1818
import { acquireLock, getRedisClient, releaseLock } from '@/lib/core/config/redis'
19+
import { validateExternalUrl } from '@/lib/core/security/input-validation'
1920
import { SSE_HEADERS } from '@/lib/core/utils/sse'
2021
import { getBaseUrl } from '@/lib/core/utils/urls'
2122
import { markExecutionCancelled } from '@/lib/execution/cancellation'
@@ -1118,17 +1119,13 @@ async function handlePushNotificationSet(
11181119
)
11191120
}
11201121

1121-
try {
1122-
const url = new URL(params.pushNotificationConfig.url)
1123-
if (url.protocol !== 'https:') {
1124-
return NextResponse.json(
1125-
createError(id, A2A_ERROR_CODES.INVALID_PARAMS, 'Push notification URL must use HTTPS'),
1126-
{ status: 400 }
1127-
)
1128-
}
1129-
} catch {
1122+
const urlValidation = validateExternalUrl(
1123+
params.pushNotificationConfig.url,
1124+
'Push notification URL'
1125+
)
1126+
if (!urlValidation.isValid) {
11301127
return NextResponse.json(
1131-
createError(id, A2A_ERROR_CODES.INVALID_PARAMS, 'Invalid push notification URL'),
1128+
createError(id, A2A_ERROR_CODES.INVALID_PARAMS, urlValidation.error || 'Invalid URL'),
11321129
{ status: 400 }
11331130
)
11341131
}

apps/sim/app/api/function/execute/route.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,14 @@ vi.mock('@/lib/execution/isolated-vm', () => ({
8484

8585
vi.mock('@sim/logger', () => loggerMock)
8686

87+
vi.mock('@/lib/auth/hybrid', () => ({
88+
checkHybridAuth: vi.fn().mockResolvedValue({
89+
success: true,
90+
userId: 'user-123',
91+
authType: 'session',
92+
}),
93+
}))
94+
8795
vi.mock('@/lib/execution/e2b', () => ({
8896
executeInE2B: vi.fn(),
8997
}))
@@ -110,6 +118,24 @@ describe('Function Execute API Route', () => {
110118
})
111119

112120
describe('Security Tests', () => {
121+
it('should reject unauthorized requests', async () => {
122+
const { checkHybridAuth } = await import('@/lib/auth/hybrid')
123+
vi.mocked(checkHybridAuth).mockResolvedValueOnce({
124+
success: false,
125+
error: 'Unauthorized',
126+
})
127+
128+
const req = createMockRequest('POST', {
129+
code: 'return "test"',
130+
})
131+
132+
const response = await POST(req)
133+
const data = await response.json()
134+
135+
expect(response.status).toBe(401)
136+
expect(data).toHaveProperty('error', 'Unauthorized')
137+
})
138+
113139
it.concurrent('should use isolated-vm for secure sandboxed execution', async () => {
114140
const req = createMockRequest('POST', {
115141
code: 'return "test"',

apps/sim/app/api/function/execute/route.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { createLogger } from '@sim/logger'
22
import { type NextRequest, NextResponse } from 'next/server'
3+
import { checkHybridAuth } from '@/lib/auth/hybrid'
34
import { isE2bEnabled } from '@/lib/core/config/feature-flags'
45
import { generateRequestId } from '@/lib/core/utils/request'
56
import { executeInE2B } from '@/lib/execution/e2b'
@@ -581,6 +582,12 @@ export async function POST(req: NextRequest) {
581582
let resolvedCode = '' // Store resolved code for error reporting
582583

583584
try {
585+
const auth = await checkHybridAuth(req)
586+
if (!auth.success || !auth.userId) {
587+
logger.warn(`[${requestId}] Unauthorized function execution attempt`)
588+
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
589+
}
590+
584591
const body = await req.json()
585592

586593
const { DEFAULT_EXECUTION_TIMEOUT_MS } = await import('@/lib/execution/constants')

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ import { account } from '@sim/db/schema'
33
import { createLogger } from '@sim/logger'
44
import { eq } from 'drizzle-orm'
55
import { type NextRequest, NextResponse } from 'next/server'
6+
import { checkHybridAuth } from '@/lib/auth/hybrid'
67
import { generateRequestId } from '@/lib/core/utils/request'
8+
import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
79
import { refreshTokenIfNeeded } from '@/app/api/auth/oauth/utils'
810
import type { StreamingExecution } from '@/executor/types'
911
import { executeProviderRequest } from '@/providers'
@@ -20,6 +22,11 @@ export async function POST(request: NextRequest) {
2022
const startTime = Date.now()
2123

2224
try {
25+
const auth = await checkHybridAuth(request, { requireWorkflowId: false })
26+
if (!auth.success || !auth.userId) {
27+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
28+
}
29+
2330
logger.info(`[${requestId}] Provider API request started`, {
2431
timestamp: new Date().toISOString(),
2532
userAgent: request.headers.get('User-Agent'),
@@ -85,6 +92,13 @@ export async function POST(request: NextRequest) {
8592
verbosity,
8693
})
8794

95+
if (workspaceId) {
96+
const workspaceAccess = await checkWorkspaceAccess(workspaceId, auth.userId)
97+
if (!workspaceAccess.hasAccess) {
98+
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
99+
}
100+
}
101+
88102
let finalApiKey: string | undefined = apiKey
89103
try {
90104
if (provider === 'vertex' && vertexCredential) {

apps/sim/app/api/tools/a2a/set-push-notification/route.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server'
33
import { z } from 'zod'
44
import { createA2AClient } from '@/lib/a2a/utils'
55
import { checkHybridAuth } from '@/lib/auth/hybrid'
6+
import { validateExternalUrl } from '@/lib/core/security/input-validation'
67
import { generateRequestId } from '@/lib/core/utils/request'
78

89
export const dynamic = 'force-dynamic'
@@ -39,6 +40,18 @@ export async function POST(request: NextRequest) {
3940
const body = await request.json()
4041
const validatedData = A2ASetPushNotificationSchema.parse(body)
4142

43+
const urlValidation = validateExternalUrl(validatedData.webhookUrl, 'Webhook URL')
44+
if (!urlValidation.isValid) {
45+
logger.warn(`[${requestId}] Invalid webhook URL`, { error: urlValidation.error })
46+
return NextResponse.json(
47+
{
48+
success: false,
49+
error: urlValidation.error,
50+
},
51+
{ status: 400 }
52+
)
53+
}
54+
4255
logger.info(`[${requestId}] A2A set push notification request`, {
4356
agentUrl: validatedData.agentUrl,
4457
taskId: validatedData.taskId,

apps/sim/app/api/tools/mysql/delete/route.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { randomUUID } from 'crypto'
22
import { createLogger } from '@sim/logger'
33
import { type NextRequest, NextResponse } from 'next/server'
44
import { z } from 'zod'
5+
import { checkHybridAuth } from '@/lib/auth/hybrid'
56
import { buildDeleteQuery, createMySQLConnection, executeQuery } from '@/app/api/tools/mysql/utils'
67

78
const logger = createLogger('MySQLDeleteAPI')
@@ -21,6 +22,12 @@ export async function POST(request: NextRequest) {
2122
const requestId = randomUUID().slice(0, 8)
2223

2324
try {
25+
const auth = await checkHybridAuth(request)
26+
if (!auth.success || !auth.userId) {
27+
logger.warn(`[${requestId}] Unauthorized MySQL delete attempt`)
28+
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
29+
}
30+
2431
const body = await request.json()
2532
const params = DeleteSchema.parse(body)
2633

apps/sim/app/api/tools/mysql/execute/route.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { randomUUID } from 'crypto'
22
import { createLogger } from '@sim/logger'
33
import { type NextRequest, NextResponse } from 'next/server'
44
import { z } from 'zod'
5+
import { checkHybridAuth } from '@/lib/auth/hybrid'
56
import { createMySQLConnection, executeQuery, validateQuery } from '@/app/api/tools/mysql/utils'
67

78
const logger = createLogger('MySQLExecuteAPI')
@@ -20,6 +21,12 @@ export async function POST(request: NextRequest) {
2021
const requestId = randomUUID().slice(0, 8)
2122

2223
try {
24+
const auth = await checkHybridAuth(request)
25+
if (!auth.success || !auth.userId) {
26+
logger.warn(`[${requestId}] Unauthorized MySQL execute attempt`)
27+
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
28+
}
29+
2330
const body = await request.json()
2431
const params = ExecuteSchema.parse(body)
2532

apps/sim/app/api/tools/mysql/insert/route.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { randomUUID } from 'crypto'
22
import { createLogger } from '@sim/logger'
33
import { type NextRequest, NextResponse } from 'next/server'
44
import { z } from 'zod'
5+
import { checkHybridAuth } from '@/lib/auth/hybrid'
56
import { buildInsertQuery, createMySQLConnection, executeQuery } from '@/app/api/tools/mysql/utils'
67

78
const logger = createLogger('MySQLInsertAPI')
@@ -42,6 +43,12 @@ export async function POST(request: NextRequest) {
4243
const requestId = randomUUID().slice(0, 8)
4344

4445
try {
46+
const auth = await checkHybridAuth(request)
47+
if (!auth.success || !auth.userId) {
48+
logger.warn(`[${requestId}] Unauthorized MySQL insert attempt`)
49+
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
50+
}
51+
4552
const body = await request.json()
4653
const params = InsertSchema.parse(body)
4754

apps/sim/app/api/tools/mysql/introspect/route.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { randomUUID } from 'crypto'
22
import { createLogger } from '@sim/logger'
33
import { type NextRequest, NextResponse } from 'next/server'
44
import { z } from 'zod'
5+
import { checkHybridAuth } from '@/lib/auth/hybrid'
56
import { createMySQLConnection, executeIntrospect } from '@/app/api/tools/mysql/utils'
67

78
const logger = createLogger('MySQLIntrospectAPI')
@@ -19,6 +20,12 @@ export async function POST(request: NextRequest) {
1920
const requestId = randomUUID().slice(0, 8)
2021

2122
try {
23+
const auth = await checkHybridAuth(request)
24+
if (!auth.success || !auth.userId) {
25+
logger.warn(`[${requestId}] Unauthorized MySQL introspect attempt`)
26+
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
27+
}
28+
2229
const body = await request.json()
2330
const params = IntrospectSchema.parse(body)
2431

0 commit comments

Comments
 (0)