Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions apps/sim/app/api/a2a/agents/[agentId]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type { AgentCapabilities, AgentSkill } from '@/lib/a2a/types'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { getRedisClient } from '@/lib/core/config/redis'
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils'

const logger = createLogger('A2AAgentCardAPI')

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

const workspaceAccess = await checkWorkspaceAccess(existingAgent.workspaceId, auth.userId)
if (!workspaceAccess.canWrite) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
}

const body = await request.json()

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

const workspaceAccess = await checkWorkspaceAccess(existingAgent.workspaceId, auth.userId)
if (!workspaceAccess.canWrite) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
}

await db.delete(a2aAgent).where(eq(a2aAgent.id, agentId))

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

const workspaceAccess = await checkWorkspaceAccess(existingAgent.workspaceId, auth.userId)
if (!workspaceAccess.canWrite) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
}

const body = await request.json()
const action = body.action as 'publish' | 'unpublish' | 'refresh'

Expand Down
17 changes: 7 additions & 10 deletions apps/sim/app/api/a2a/serve/[agentId]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { getBrandConfig } from '@/lib/branding/branding'
import { acquireLock, getRedisClient, releaseLock } from '@/lib/core/config/redis'
import { validateExternalUrl } from '@/lib/core/security/input-validation'
import { SSE_HEADERS } from '@/lib/core/utils/sse'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { markExecutionCancelled } from '@/lib/execution/cancellation'
Expand Down Expand Up @@ -1118,17 +1119,13 @@ async function handlePushNotificationSet(
)
}

try {
const url = new URL(params.pushNotificationConfig.url)
if (url.protocol !== 'https:') {
return NextResponse.json(
createError(id, A2A_ERROR_CODES.INVALID_PARAMS, 'Push notification URL must use HTTPS'),
{ status: 400 }
)
}
} catch {
const urlValidation = validateExternalUrl(
params.pushNotificationConfig.url,
'Push notification URL'
)
if (!urlValidation.isValid) {
return NextResponse.json(
createError(id, A2A_ERROR_CODES.INVALID_PARAMS, 'Invalid push notification URL'),
createError(id, A2A_ERROR_CODES.INVALID_PARAMS, urlValidation.error || 'Invalid URL'),
{ status: 400 }
)
}
Expand Down
26 changes: 26 additions & 0 deletions apps/sim/app/api/function/execute/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,14 @@ vi.mock('@/lib/execution/isolated-vm', () => ({

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

vi.mock('@/lib/auth/hybrid', () => ({
checkHybridAuth: vi.fn().mockResolvedValue({
success: true,
userId: 'user-123',
authType: 'session',
}),
}))

vi.mock('@/lib/execution/e2b', () => ({
executeInE2B: vi.fn(),
}))
Expand All @@ -110,6 +118,24 @@ describe('Function Execute API Route', () => {
})

describe('Security Tests', () => {
it('should reject unauthorized requests', async () => {
const { checkHybridAuth } = await import('@/lib/auth/hybrid')
vi.mocked(checkHybridAuth).mockResolvedValueOnce({
success: false,
error: 'Unauthorized',
})

const req = createMockRequest('POST', {
code: 'return "test"',
})

const response = await POST(req)
const data = await response.json()

expect(response.status).toBe(401)
expect(data).toHaveProperty('error', 'Unauthorized')
})

it.concurrent('should use isolated-vm for secure sandboxed execution', async () => {
const req = createMockRequest('POST', {
code: 'return "test"',
Expand Down
7 changes: 7 additions & 0 deletions apps/sim/app/api/function/execute/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { isE2bEnabled } from '@/lib/core/config/feature-flags'
import { generateRequestId } from '@/lib/core/utils/request'
import { executeInE2B } from '@/lib/execution/e2b'
Expand Down Expand Up @@ -581,6 +582,12 @@ export async function POST(req: NextRequest) {
let resolvedCode = '' // Store resolved code for error reporting

try {
const auth = await checkHybridAuth(req)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized function execution attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}

const body = await req.json()

const { DEFAULT_EXECUTION_TIMEOUT_MS } = await import('@/lib/execution/constants')
Expand Down
14 changes: 14 additions & 0 deletions apps/sim/app/api/providers/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import { account } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
import { refreshTokenIfNeeded } from '@/app/api/auth/oauth/utils'
import type { StreamingExecution } from '@/executor/types'
import { executeProviderRequest } from '@/providers'
Expand All @@ -20,6 +22,11 @@ export async function POST(request: NextRequest) {
const startTime = Date.now()

try {
const auth = await checkHybridAuth(request, { requireWorkflowId: false })
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}

logger.info(`[${requestId}] Provider API request started`, {
timestamp: new Date().toISOString(),
userAgent: request.headers.get('User-Agent'),
Expand Down Expand Up @@ -85,6 +92,13 @@ export async function POST(request: NextRequest) {
verbosity,
})

if (workspaceId) {
const workspaceAccess = await checkWorkspaceAccess(workspaceId, auth.userId)
if (!workspaceAccess.hasAccess) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
}
}

let finalApiKey: string | undefined = apiKey
try {
if (provider === 'vertex' && vertexCredential) {
Expand Down
13 changes: 13 additions & 0 deletions apps/sim/app/api/tools/a2a/set-push-notification/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { createA2AClient } from '@/lib/a2a/utils'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { validateExternalUrl } from '@/lib/core/security/input-validation'
import { generateRequestId } from '@/lib/core/utils/request'

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

const urlValidation = validateExternalUrl(validatedData.webhookUrl, 'Webhook URL')
if (!urlValidation.isValid) {
logger.warn(`[${requestId}] Invalid webhook URL`, { error: urlValidation.error })
return NextResponse.json(
{
success: false,
error: urlValidation.error,
},
{ status: 400 }
)
}

logger.info(`[${requestId}] A2A set push notification request`, {
agentUrl: validatedData.agentUrl,
taskId: validatedData.taskId,
Expand Down
7 changes: 7 additions & 0 deletions apps/sim/app/api/tools/mysql/delete/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { randomUUID } from 'crypto'
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { buildDeleteQuery, createMySQLConnection, executeQuery } from '@/app/api/tools/mysql/utils'

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

try {
const auth = await checkHybridAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized MySQL delete attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}

const body = await request.json()
const params = DeleteSchema.parse(body)

Expand Down
7 changes: 7 additions & 0 deletions apps/sim/app/api/tools/mysql/execute/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { randomUUID } from 'crypto'
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { createMySQLConnection, executeQuery, validateQuery } from '@/app/api/tools/mysql/utils'

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

try {
const auth = await checkHybridAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized MySQL execute attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}

const body = await request.json()
const params = ExecuteSchema.parse(body)

Expand Down
7 changes: 7 additions & 0 deletions apps/sim/app/api/tools/mysql/insert/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { randomUUID } from 'crypto'
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { buildInsertQuery, createMySQLConnection, executeQuery } from '@/app/api/tools/mysql/utils'

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

try {
const auth = await checkHybridAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized MySQL insert attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}

const body = await request.json()
const params = InsertSchema.parse(body)

Expand Down
7 changes: 7 additions & 0 deletions apps/sim/app/api/tools/mysql/introspect/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { randomUUID } from 'crypto'
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { createMySQLConnection, executeIntrospect } from '@/app/api/tools/mysql/utils'

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

try {
const auth = await checkHybridAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized MySQL introspect attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}

const body = await request.json()
const params = IntrospectSchema.parse(body)

Expand Down
7 changes: 7 additions & 0 deletions apps/sim/app/api/tools/mysql/query/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { randomUUID } from 'crypto'
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { createMySQLConnection, executeQuery, validateQuery } from '@/app/api/tools/mysql/utils'

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

try {
const auth = await checkHybridAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized MySQL query attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}

const body = await request.json()
const params = QuerySchema.parse(body)

Expand Down
7 changes: 7 additions & 0 deletions apps/sim/app/api/tools/mysql/update/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { randomUUID } from 'crypto'
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { buildUpdateQuery, createMySQLConnection, executeQuery } from '@/app/api/tools/mysql/utils'

const logger = createLogger('MySQLUpdateAPI')
Expand Down Expand Up @@ -40,6 +41,12 @@ export async function POST(request: NextRequest) {
const requestId = randomUUID().slice(0, 8)

try {
const auth = await checkHybridAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized MySQL update attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}

const body = await request.json()
const params = UpdateSchema.parse(body)

Expand Down
7 changes: 7 additions & 0 deletions apps/sim/app/api/tools/postgresql/delete/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { randomUUID } from 'crypto'
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { createPostgresConnection, executeDelete } from '@/app/api/tools/postgresql/utils'

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

try {
const auth = await checkHybridAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized PostgreSQL delete attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}

const body = await request.json()
const params = DeleteSchema.parse(body)

Expand Down
7 changes: 7 additions & 0 deletions apps/sim/app/api/tools/postgresql/execute/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { randomUUID } from 'crypto'
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import {
createPostgresConnection,
executeQuery,
Expand All @@ -24,6 +25,12 @@ export async function POST(request: NextRequest) {
const requestId = randomUUID().slice(0, 8)

try {
const auth = await checkHybridAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized PostgreSQL execute attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}

const body = await request.json()
const params = ExecuteSchema.parse(body)

Expand Down
7 changes: 7 additions & 0 deletions apps/sim/app/api/tools/postgresql/insert/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { randomUUID } from 'crypto'
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { createPostgresConnection, executeInsert } from '@/app/api/tools/postgresql/utils'

const logger = createLogger('PostgreSQLInsertAPI')
Expand Down Expand Up @@ -42,6 +43,12 @@ export async function POST(request: NextRequest) {
const requestId = randomUUID().slice(0, 8)

try {
const auth = await checkHybridAuth(request)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized PostgreSQL insert attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}

const body = await request.json()

const params = InsertSchema.parse(body)
Expand Down
Loading