Skip to content

Commit 48adaa0

Browse files
authored
fix(security): restrict API key access on internal-only routes (#2964)
* fix(security): restrict API key access on internal-only routes * test(security): update function execute tests for checkInternalAuth * updated agent handler * move session check higher in checkSessionOrInternalAuth * extracted duplicate code into helper for resolving user from jwt
1 parent 211a7ac commit 48adaa0

File tree

88 files changed

+575
-758
lines changed

Some content is hidden

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

88 files changed

+575
-758
lines changed

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,10 +85,10 @@ vi.mock('@/lib/execution/isolated-vm', () => ({
8585
vi.mock('@sim/logger', () => loggerMock)
8686

8787
vi.mock('@/lib/auth/hybrid', () => ({
88-
checkHybridAuth: vi.fn().mockResolvedValue({
88+
checkInternalAuth: vi.fn().mockResolvedValue({
8989
success: true,
9090
userId: 'user-123',
91-
authType: 'session',
91+
authType: 'internal_jwt',
9292
}),
9393
}))
9494

@@ -119,8 +119,8 @@ describe('Function Execute API Route', () => {
119119

120120
describe('Security Tests', () => {
121121
it('should reject unauthorized requests', async () => {
122-
const { checkHybridAuth } = await import('@/lib/auth/hybrid')
123-
vi.mocked(checkHybridAuth).mockResolvedValueOnce({
122+
const { checkInternalAuth } = await import('@/lib/auth/hybrid')
123+
vi.mocked(checkInternalAuth).mockResolvedValueOnce({
124124
success: false,
125125
error: 'Unauthorized',
126126
})

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

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

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

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ 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'
6+
import { checkInternalAuth } from '@/lib/auth/hybrid'
77
import { generateRequestId } from '@/lib/core/utils/request'
88
import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
99
import { refreshTokenIfNeeded } from '@/app/api/auth/oauth/utils'
@@ -22,7 +22,7 @@ export async function POST(request: NextRequest) {
2222
const startTime = Date.now()
2323

2424
try {
25-
const auth = await checkHybridAuth(request, { requireWorkflowId: false })
25+
const auth = await checkInternalAuth(request, { requireWorkflowId: false })
2626
if (!auth.success || !auth.userId) {
2727
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
2828
}

apps/sim/app/api/tools/custom/route.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ describe('Custom Tools API Routes', () => {
181181
}))
182182

183183
vi.doMock('@/lib/auth/hybrid', () => ({
184-
checkHybridAuth: vi.fn().mockResolvedValue({
184+
checkSessionOrInternalAuth: vi.fn().mockResolvedValue({
185185
success: true,
186186
userId: 'user-123',
187187
authType: 'session',
@@ -254,7 +254,7 @@ describe('Custom Tools API Routes', () => {
254254
)
255255

256256
vi.doMock('@/lib/auth/hybrid', () => ({
257-
checkHybridAuth: vi.fn().mockResolvedValue({
257+
checkSessionOrInternalAuth: vi.fn().mockResolvedValue({
258258
success: false,
259259
error: 'Unauthorized',
260260
}),
@@ -304,7 +304,7 @@ describe('Custom Tools API Routes', () => {
304304
describe('POST /api/tools/custom', () => {
305305
it('should reject unauthorized requests', async () => {
306306
vi.doMock('@/lib/auth/hybrid', () => ({
307-
checkHybridAuth: vi.fn().mockResolvedValue({
307+
checkSessionOrInternalAuth: vi.fn().mockResolvedValue({
308308
success: false,
309309
error: 'Unauthorized',
310310
}),
@@ -390,7 +390,7 @@ describe('Custom Tools API Routes', () => {
390390

391391
it('should prevent unauthorized deletion of user-scoped tool', async () => {
392392
vi.doMock('@/lib/auth/hybrid', () => ({
393-
checkHybridAuth: vi.fn().mockResolvedValue({
393+
checkSessionOrInternalAuth: vi.fn().mockResolvedValue({
394394
success: true,
395395
userId: 'user-456',
396396
authType: 'session',
@@ -413,7 +413,7 @@ describe('Custom Tools API Routes', () => {
413413

414414
it('should reject unauthorized requests', async () => {
415415
vi.doMock('@/lib/auth/hybrid', () => ({
416-
checkHybridAuth: vi.fn().mockResolvedValue({
416+
checkSessionOrInternalAuth: vi.fn().mockResolvedValue({
417417
success: false,
418418
error: 'Unauthorized',
419419
}),

apps/sim/app/api/tools/custom/route.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { createLogger } from '@sim/logger'
44
import { and, desc, eq, isNull, or } from 'drizzle-orm'
55
import { type NextRequest, NextResponse } from 'next/server'
66
import { z } from 'zod'
7-
import { checkHybridAuth } from '@/lib/auth/hybrid'
7+
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
88
import { generateRequestId } from '@/lib/core/utils/request'
99
import { upsertCustomTools } from '@/lib/workflows/custom-tools/operations'
1010
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
@@ -42,8 +42,8 @@ export async function GET(request: NextRequest) {
4242
const workflowId = searchParams.get('workflowId')
4343

4444
try {
45-
// Use hybrid auth to support session, API key, and internal JWT
46-
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
45+
// Use session/internal auth to support session and internal JWT (no API key access)
46+
const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
4747
if (!authResult.success || !authResult.userId) {
4848
logger.warn(`[${requestId}] Unauthorized custom tools access attempt`)
4949
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
@@ -69,8 +69,8 @@ export async function GET(request: NextRequest) {
6969
}
7070

7171
// Check workspace permissions
72-
// For internal JWT with workflowId: checkHybridAuth already resolved userId from workflow owner
73-
// For session/API key: verify user has access to the workspace
72+
// For internal JWT with workflowId: checkSessionOrInternalAuth already resolved userId from workflow owner
73+
// For session: verify user has access to the workspace
7474
// For legacy (no workspaceId): skip workspace check, rely on userId match
7575
if (resolvedWorkspaceId && !(authResult.authType === 'internal_jwt' && workflowId)) {
7676
const userPermission = await getUserEntityPermissions(
@@ -116,8 +116,8 @@ export async function POST(req: NextRequest) {
116116
const requestId = generateRequestId()
117117

118118
try {
119-
// Use hybrid auth (though this endpoint is only called from UI)
120-
const authResult = await checkHybridAuth(req, { requireWorkflowId: false })
119+
// Use session/internal auth (no API key access)
120+
const authResult = await checkSessionOrInternalAuth(req, { requireWorkflowId: false })
121121
if (!authResult.success || !authResult.userId) {
122122
logger.warn(`[${requestId}] Unauthorized custom tools update attempt`)
123123
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
@@ -193,8 +193,8 @@ export async function DELETE(request: NextRequest) {
193193
}
194194

195195
try {
196-
// Use hybrid auth (though this endpoint is only called from UI)
197-
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
196+
// Use session/internal auth (no API key access)
197+
const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
198198
if (!authResult.success || !authResult.userId) {
199199
logger.warn(`[${requestId}] Unauthorized custom tool deletion attempt`)
200200
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })

apps/sim/app/api/tools/discord/send-message/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { createLogger } from '@sim/logger'
22
import { type NextRequest, NextResponse } from 'next/server'
33
import { z } from 'zod'
4-
import { checkHybridAuth } from '@/lib/auth/hybrid'
4+
import { checkInternalAuth } from '@/lib/auth/hybrid'
55
import { validateNumericId } from '@/lib/core/security/input-validation'
66
import { generateRequestId } from '@/lib/core/utils/request'
77
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
@@ -22,7 +22,7 @@ export async function POST(request: NextRequest) {
2222
const requestId = generateRequestId()
2323

2424
try {
25-
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
25+
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
2626

2727
if (!authResult.success) {
2828
logger.warn(`[${requestId}] Unauthorized Discord send attempt: ${authResult.error}`)

apps/sim/app/api/tools/gmail/add-label/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { createLogger } from '@sim/logger'
22
import { type NextRequest, NextResponse } from 'next/server'
33
import { z } from 'zod'
4-
import { checkHybridAuth } from '@/lib/auth/hybrid'
4+
import { checkInternalAuth } from '@/lib/auth/hybrid'
55
import { validateAlphanumericId } from '@/lib/core/security/input-validation'
66
import { generateRequestId } from '@/lib/core/utils/request'
77

@@ -21,7 +21,7 @@ export async function POST(request: NextRequest) {
2121
const requestId = generateRequestId()
2222

2323
try {
24-
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
24+
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
2525

2626
if (!authResult.success) {
2727
logger.warn(`[${requestId}] Unauthorized Gmail add label attempt: ${authResult.error}`)

apps/sim/app/api/tools/gmail/archive/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { createLogger } from '@sim/logger'
22
import { type NextRequest, NextResponse } from 'next/server'
33
import { z } from 'zod'
4-
import { checkHybridAuth } from '@/lib/auth/hybrid'
4+
import { checkInternalAuth } from '@/lib/auth/hybrid'
55
import { generateRequestId } from '@/lib/core/utils/request'
66

77
export const dynamic = 'force-dynamic'
@@ -19,7 +19,7 @@ export async function POST(request: NextRequest) {
1919
const requestId = generateRequestId()
2020

2121
try {
22-
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
22+
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
2323

2424
if (!authResult.success) {
2525
logger.warn(`[${requestId}] Unauthorized Gmail archive attempt: ${authResult.error}`)

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { createLogger } from '@sim/logger'
22
import { type NextRequest, NextResponse } from 'next/server'
33
import { z } from 'zod'
4-
import { checkHybridAuth } from '@/lib/auth/hybrid'
4+
import { checkInternalAuth } from '@/lib/auth/hybrid'
55
import { generateRequestId } from '@/lib/core/utils/request'
66

77
export const dynamic = 'force-dynamic'
@@ -19,7 +19,7 @@ export async function POST(request: NextRequest) {
1919
const requestId = generateRequestId()
2020

2121
try {
22-
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
22+
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
2323

2424
if (!authResult.success) {
2525
logger.warn(`[${requestId}] Unauthorized Gmail delete attempt: ${authResult.error}`)

apps/sim/app/api/tools/gmail/draft/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { createLogger } from '@sim/logger'
22
import { type NextRequest, NextResponse } from 'next/server'
33
import { z } from 'zod'
4-
import { checkHybridAuth } from '@/lib/auth/hybrid'
4+
import { checkInternalAuth } from '@/lib/auth/hybrid'
55
import { generateRequestId } from '@/lib/core/utils/request'
66
import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils'
77
import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
@@ -35,7 +35,7 @@ export async function POST(request: NextRequest) {
3535
const requestId = generateRequestId()
3636

3737
try {
38-
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
38+
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
3939

4040
if (!authResult.success) {
4141
logger.warn(`[${requestId}] Unauthorized Gmail draft attempt: ${authResult.error}`)

0 commit comments

Comments
 (0)