Skip to content

Commit fbafe20

Browse files
authored
fix(confluence): add input validation for SSRF-flagged parameters (#3351)
1 parent ba7d6ff commit fbafe20

File tree

7 files changed

+162
-16
lines changed

7 files changed

+162
-16
lines changed

apps/sim/app/api/tools/confluence/page-descendants/route.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { createLogger } from '@sim/logger'
22
import { type NextRequest, NextResponse } from 'next/server'
33
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
4-
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
4+
import {
5+
validateAlphanumericId,
6+
validateJiraCloudId,
7+
validatePaginationCursor,
8+
} from '@/lib/core/security/input-validation'
59
import { getConfluenceCloudId } from '@/tools/confluence/utils'
610

711
const logger = createLogger('ConfluencePageDescendantsAPI')
@@ -50,6 +54,10 @@ export async function POST(request: NextRequest) {
5054
queryParams.append('limit', String(Math.min(limit, 250)))
5155

5256
if (cursor) {
57+
const cursorValidation = validatePaginationCursor(cursor, 'cursor')
58+
if (!cursorValidation.isValid) {
59+
return NextResponse.json({ error: cursorValidation.error }, { status: 400 })
60+
}
5361
queryParams.append('cursor', cursor)
5462
}
5563

apps/sim/app/api/tools/confluence/page-versions/route.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import { createLogger } from '@sim/logger'
22
import { type NextRequest, NextResponse } from 'next/server'
33
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
4-
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
4+
import {
5+
validateAlphanumericId,
6+
validateJiraCloudId,
7+
validateNumericId,
8+
validatePaginationCursor,
9+
} from '@/lib/core/security/input-validation'
510
import { cleanHtmlContent, getConfluenceCloudId } from '@/tools/confluence/utils'
611

712
const logger = createLogger('ConfluencePageVersionsAPI')
@@ -57,8 +62,14 @@ export async function POST(request: NextRequest) {
5762

5863
// If versionNumber is provided, get specific version with page content
5964
if (versionNumber !== undefined && versionNumber !== null) {
60-
const versionUrl = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/versions/${versionNumber}`
61-
const pageUrl = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}?version=${versionNumber}&body-format=storage`
65+
const versionValidation = validateNumericId(versionNumber, 'versionNumber', { min: 1 })
66+
if (!versionValidation.isValid) {
67+
return NextResponse.json({ error: versionValidation.error }, { status: 400 })
68+
}
69+
const safeVersion = versionValidation.sanitized
70+
71+
const versionUrl = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/versions/${safeVersion}`
72+
const pageUrl = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}?version=${safeVersion}&body-format=storage`
6273

6374
logger.info(`Fetching version ${versionNumber} for page ${pageId}`)
6475

@@ -135,6 +146,10 @@ export async function POST(request: NextRequest) {
135146
queryParams.append('limit', String(Math.min(limit, 250)))
136147

137148
if (cursor) {
149+
const cursorValidation = validatePaginationCursor(cursor, 'cursor')
150+
if (!cursorValidation.isValid) {
151+
return NextResponse.json({ error: cursorValidation.error }, { status: 400 })
152+
}
138153
queryParams.append('cursor', cursor)
139154
}
140155

apps/sim/app/api/tools/confluence/space-permissions/route.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { createLogger } from '@sim/logger'
22
import { type NextRequest, NextResponse } from 'next/server'
33
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
4-
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
4+
import {
5+
validateAlphanumericId,
6+
validateJiraCloudId,
7+
validatePaginationCursor,
8+
} from '@/lib/core/security/input-validation'
59
import { getConfluenceCloudId } from '@/tools/confluence/utils'
610

711
const logger = createLogger('ConfluenceSpacePermissionsAPI')
@@ -50,6 +54,10 @@ export async function POST(request: NextRequest) {
5054
queryParams.append('limit', String(Math.min(limit, 250)))
5155

5256
if (cursor) {
57+
const cursorValidation = validatePaginationCursor(cursor, 'cursor')
58+
if (!cursorValidation.isValid) {
59+
return NextResponse.json({ error: cursorValidation.error }, { status: 400 })
60+
}
5361
queryParams.append('cursor', cursor)
5462
}
5563

apps/sim/app/api/tools/confluence/space-properties/route.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { createLogger } from '@sim/logger'
22
import { type NextRequest, NextResponse } from 'next/server'
33
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
4-
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
4+
import {
5+
validateAlphanumericId,
6+
validateJiraCloudId,
7+
validatePaginationCursor,
8+
} from '@/lib/core/security/input-validation'
59
import { getConfluenceCloudId } from '@/tools/confluence/utils'
610

711
const logger = createLogger('ConfluenceSpacePropertiesAPI')
@@ -82,7 +86,7 @@ export async function POST(request: NextRequest) {
8286
return NextResponse.json({ error: propertyIdValidation.error }, { status: 400 })
8387
}
8488

85-
const url = `${baseUrl}/${propertyId}`
89+
const url = `${baseUrl}/${encodeURIComponent(propertyId)}`
8690

8791
logger.info(`Deleting space property ${propertyId} from space ${spaceId}`)
8892

@@ -148,7 +152,13 @@ export async function POST(request: NextRequest) {
148152
const queryParams = new URLSearchParams()
149153
queryParams.append('limit', String(Math.min(limit, 250)))
150154

151-
if (cursor) queryParams.append('cursor', cursor)
155+
if (cursor) {
156+
const cursorValidation = validatePaginationCursor(cursor, 'cursor')
157+
if (!cursorValidation.isValid) {
158+
return NextResponse.json({ error: cursorValidation.error }, { status: 400 })
159+
}
160+
queryParams.append('cursor', cursor)
161+
}
152162

153163
const url = `${baseUrl}?${queryParams.toString()}`
154164

apps/sim/app/api/tools/confluence/tasks/route.ts

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import { createLogger } from '@sim/logger'
22
import { type NextRequest, NextResponse } from 'next/server'
33
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
4-
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
4+
import {
5+
validateAlphanumericId,
6+
validateJiraCloudId,
7+
validatePaginationCursor,
8+
validatePathSegment,
9+
} from '@/lib/core/security/input-validation'
510
import { getConfluenceCloudId } from '@/tools/confluence/utils'
611

712
const logger = createLogger('ConfluenceTasksAPI')
@@ -180,11 +185,40 @@ export async function POST(request: NextRequest) {
180185
const queryParams = new URLSearchParams()
181186
queryParams.append('limit', String(Math.min(limit, 250)))
182187

183-
if (cursor) queryParams.append('cursor', cursor)
188+
if (cursor) {
189+
const cursorValidation = validatePaginationCursor(cursor, 'cursor')
190+
if (!cursorValidation.isValid) {
191+
return NextResponse.json({ error: cursorValidation.error }, { status: 400 })
192+
}
193+
queryParams.append('cursor', cursor)
194+
}
184195
if (taskStatus) queryParams.append('status', taskStatus)
185-
if (pageId) queryParams.append('page-id', pageId)
186-
if (spaceId) queryParams.append('space-id', spaceId)
187-
if (assignedTo) queryParams.append('assigned-to', assignedTo)
196+
if (pageId) {
197+
const pageIdValidation = validateAlphanumericId(pageId, 'pageId', 255)
198+
if (!pageIdValidation.isValid) {
199+
return NextResponse.json({ error: pageIdValidation.error }, { status: 400 })
200+
}
201+
queryParams.append('page-id', pageId)
202+
}
203+
if (spaceId) {
204+
const spaceIdValidation = validateAlphanumericId(spaceId, 'spaceId', 255)
205+
if (!spaceIdValidation.isValid) {
206+
return NextResponse.json({ error: spaceIdValidation.error }, { status: 400 })
207+
}
208+
queryParams.append('space-id', spaceId)
209+
}
210+
if (assignedTo) {
211+
// Atlassian account IDs: 5d5bd05c3aee0123abc or 557058:6b9c9931-4693-49c1-8b3a-931f1af98134
212+
const assignedToValidation = validatePathSegment(assignedTo, {
213+
paramName: 'assignedTo',
214+
maxLength: 128,
215+
customPattern: /^[a-zA-Z0-9_|:-]+$/,
216+
})
217+
if (!assignedToValidation.isValid) {
218+
return NextResponse.json({ error: assignedToValidation.error }, { status: 400 })
219+
}
220+
queryParams.append('assigned-to', assignedTo)
221+
}
188222

189223
const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/tasks?${queryParams.toString()}`
190224

apps/sim/app/api/tools/confluence/user/route.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,11 @@ export async function POST(request: NextRequest) {
3434
return NextResponse.json({ error: 'Account ID is required' }, { status: 400 })
3535
}
3636

37-
// Atlassian account IDs use format like 557058:6b9c9931-4693-49c1-8b3a-931f1af98134
37+
// Atlassian account IDs: 5d5bd05c3aee0123abc or 557058:6b9c9931-4693-49c1-8b3a-931f1af98134
3838
const accountIdValidation = validatePathSegment(accountId, {
3939
paramName: 'accountId',
40-
maxLength: 255,
41-
customPattern: /^[a-zA-Z0-9:-]+$/,
40+
maxLength: 128,
41+
customPattern: /^[a-zA-Z0-9_|:-]+$/,
4242
})
4343
if (!accountIdValidation.isValid) {
4444
return NextResponse.json({ error: accountIdValidation.error }, { status: 400 })

apps/sim/lib/core/security/input-validation.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1039,3 +1039,74 @@ export function validateGoogleCalendarId(
10391039

10401040
return { isValid: true, sanitized: value }
10411041
}
1042+
1043+
/**
1044+
* Validates a pagination cursor token
1045+
*
1046+
* Pagination cursors are opaque tokens returned by APIs (e.g., Confluence, Jira)
1047+
* and passed back to get the next page. They are typically base64-encoded or
1048+
* URL-safe strings. This validator ensures the cursor cannot contain characters
1049+
* that could alter URL structure.
1050+
*
1051+
* @param value - The cursor token to validate
1052+
* @param paramName - Name of the parameter for error messages
1053+
* @param maxLength - Maximum length (default: 1024)
1054+
* @returns ValidationResult
1055+
*
1056+
* @example
1057+
* ```typescript
1058+
* if (cursor) {
1059+
* const result = validatePaginationCursor(cursor, 'cursor')
1060+
* if (!result.isValid) {
1061+
* return NextResponse.json({ error: result.error }, { status: 400 })
1062+
* }
1063+
* }
1064+
* ```
1065+
*/
1066+
export function validatePaginationCursor(
1067+
value: string | null | undefined,
1068+
paramName = 'cursor',
1069+
maxLength = 1024
1070+
): ValidationResult {
1071+
if (value === null || value === undefined || value === '') {
1072+
return {
1073+
isValid: false,
1074+
error: `${paramName} is required`,
1075+
}
1076+
}
1077+
1078+
if (value.length > maxLength) {
1079+
logger.warn('Pagination cursor exceeds maximum length', {
1080+
paramName,
1081+
length: value.length,
1082+
maxLength,
1083+
})
1084+
return {
1085+
isValid: false,
1086+
error: `${paramName} exceeds maximum length of ${maxLength} characters`,
1087+
}
1088+
}
1089+
1090+
if (/[\x00-\x1f\x7f]/.test(value) || value.includes('%00')) {
1091+
logger.warn('Pagination cursor contains control characters', { paramName })
1092+
return {
1093+
isValid: false,
1094+
error: `${paramName} contains invalid characters`,
1095+
}
1096+
}
1097+
1098+
// Allow alphanumeric, base64 chars (+, /, =), and URL-safe chars (-, _, ., ~, %)
1099+
const cursorPattern = /^[A-Za-z0-9+/=\-_.~%]+$/
1100+
if (!cursorPattern.test(value)) {
1101+
logger.warn('Pagination cursor contains disallowed characters', {
1102+
paramName,
1103+
value: value.substring(0, 100),
1104+
})
1105+
return {
1106+
isValid: false,
1107+
error: `${paramName} contains invalid characters`,
1108+
}
1109+
}
1110+
1111+
return { isValid: true, sanitized: value }
1112+
}

0 commit comments

Comments
 (0)