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
10 changes: 9 additions & 1 deletion apps/sim/app/api/tools/confluence/page-descendants/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
import {
validateAlphanumericId,
validateJiraCloudId,
validatePaginationCursor,
} from '@/lib/core/security/input-validation'
import { getConfluenceCloudId } from '@/tools/confluence/utils'

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

if (cursor) {
const cursorValidation = validatePaginationCursor(cursor, 'cursor')
if (!cursorValidation.isValid) {
return NextResponse.json({ error: cursorValidation.error }, { status: 400 })
}
queryParams.append('cursor', cursor)
}

Expand Down
21 changes: 18 additions & 3 deletions apps/sim/app/api/tools/confluence/page-versions/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
import {
validateAlphanumericId,
validateJiraCloudId,
validateNumericId,
validatePaginationCursor,
} from '@/lib/core/security/input-validation'
import { cleanHtmlContent, getConfluenceCloudId } from '@/tools/confluence/utils'

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

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

const versionUrl = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}/versions/${safeVersion}`
const pageUrl = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/api/v2/pages/${pageId}?version=${safeVersion}&body-format=storage`

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

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

if (cursor) {
const cursorValidation = validatePaginationCursor(cursor, 'cursor')
if (!cursorValidation.isValid) {
return NextResponse.json({ error: cursorValidation.error }, { status: 400 })
}
queryParams.append('cursor', cursor)
}

Expand Down
10 changes: 9 additions & 1 deletion apps/sim/app/api/tools/confluence/space-permissions/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
import {
validateAlphanumericId,
validateJiraCloudId,
validatePaginationCursor,
} from '@/lib/core/security/input-validation'
import { getConfluenceCloudId } from '@/tools/confluence/utils'

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

if (cursor) {
const cursorValidation = validatePaginationCursor(cursor, 'cursor')
if (!cursorValidation.isValid) {
return NextResponse.json({ error: cursorValidation.error }, { status: 400 })
}
queryParams.append('cursor', cursor)
}

Expand Down
16 changes: 13 additions & 3 deletions apps/sim/app/api/tools/confluence/space-properties/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
import {
validateAlphanumericId,
validateJiraCloudId,
validatePaginationCursor,
} from '@/lib/core/security/input-validation'
import { getConfluenceCloudId } from '@/tools/confluence/utils'

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

const url = `${baseUrl}/${propertyId}`
const url = `${baseUrl}/${encodeURIComponent(propertyId)}`

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

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

if (cursor) queryParams.append('cursor', cursor)
if (cursor) {
const cursorValidation = validatePaginationCursor(cursor, 'cursor')
if (!cursorValidation.isValid) {
return NextResponse.json({ error: cursorValidation.error }, { status: 400 })
}
queryParams.append('cursor', cursor)
}

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

Expand Down
44 changes: 39 additions & 5 deletions apps/sim/app/api/tools/confluence/tasks/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation'
import {
validateAlphanumericId,
validateJiraCloudId,
validatePaginationCursor,
validatePathSegment,
} from '@/lib/core/security/input-validation'
import { getConfluenceCloudId } from '@/tools/confluence/utils'

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

if (cursor) queryParams.append('cursor', cursor)
if (cursor) {
const cursorValidation = validatePaginationCursor(cursor, 'cursor')
if (!cursorValidation.isValid) {
return NextResponse.json({ error: cursorValidation.error }, { status: 400 })
}
queryParams.append('cursor', cursor)
}
if (taskStatus) queryParams.append('status', taskStatus)
if (pageId) queryParams.append('page-id', pageId)
if (spaceId) queryParams.append('space-id', spaceId)
if (assignedTo) queryParams.append('assigned-to', assignedTo)
if (pageId) {
const pageIdValidation = validateAlphanumericId(pageId, 'pageId', 255)
if (!pageIdValidation.isValid) {
return NextResponse.json({ error: pageIdValidation.error }, { status: 400 })
}
queryParams.append('page-id', pageId)
}
if (spaceId) {
const spaceIdValidation = validateAlphanumericId(spaceId, 'spaceId', 255)
if (!spaceIdValidation.isValid) {
return NextResponse.json({ error: spaceIdValidation.error }, { status: 400 })
}
queryParams.append('space-id', spaceId)
}
if (assignedTo) {
// Atlassian account IDs: 5d5bd05c3aee0123abc or 557058:6b9c9931-4693-49c1-8b3a-931f1af98134
const assignedToValidation = validatePathSegment(assignedTo, {
paramName: 'assignedTo',
maxLength: 128,
customPattern: /^[a-zA-Z0-9_|:-]+$/,
})
if (!assignedToValidation.isValid) {
return NextResponse.json({ error: assignedToValidation.error }, { status: 400 })
}
queryParams.append('assigned-to', assignedTo)
}

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

Expand Down
6 changes: 3 additions & 3 deletions apps/sim/app/api/tools/confluence/user/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,11 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: 'Account ID is required' }, { status: 400 })
}

// Atlassian account IDs use format like 557058:6b9c9931-4693-49c1-8b3a-931f1af98134
// Atlassian account IDs: 5d5bd05c3aee0123abc or 557058:6b9c9931-4693-49c1-8b3a-931f1af98134
const accountIdValidation = validatePathSegment(accountId, {
paramName: 'accountId',
maxLength: 255,
customPattern: /^[a-zA-Z0-9:-]+$/,
maxLength: 128,
customPattern: /^[a-zA-Z0-9_|:-]+$/,
})
if (!accountIdValidation.isValid) {
return NextResponse.json({ error: accountIdValidation.error }, { status: 400 })
Expand Down
71 changes: 71 additions & 0 deletions apps/sim/lib/core/security/input-validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1039,3 +1039,74 @@ export function validateGoogleCalendarId(

return { isValid: true, sanitized: value }
}

/**
* Validates a pagination cursor token
*
* Pagination cursors are opaque tokens returned by APIs (e.g., Confluence, Jira)
* and passed back to get the next page. They are typically base64-encoded or
* URL-safe strings. This validator ensures the cursor cannot contain characters
* that could alter URL structure.
*
* @param value - The cursor token to validate
* @param paramName - Name of the parameter for error messages
* @param maxLength - Maximum length (default: 1024)
* @returns ValidationResult
*
* @example
* ```typescript
* if (cursor) {
* const result = validatePaginationCursor(cursor, 'cursor')
* if (!result.isValid) {
* return NextResponse.json({ error: result.error }, { status: 400 })
* }
* }
* ```
*/
export function validatePaginationCursor(
value: string | null | undefined,
paramName = 'cursor',
maxLength = 1024
): ValidationResult {
if (value === null || value === undefined || value === '') {
return {
isValid: false,
error: `${paramName} is required`,
}
}

if (value.length > maxLength) {
logger.warn('Pagination cursor exceeds maximum length', {
paramName,
length: value.length,
maxLength,
})
return {
isValid: false,
error: `${paramName} exceeds maximum length of ${maxLength} characters`,
}
}

if (/[\x00-\x1f\x7f]/.test(value) || value.includes('%00')) {
logger.warn('Pagination cursor contains control characters', { paramName })
return {
isValid: false,
error: `${paramName} contains invalid characters`,
}
}

// Allow alphanumeric, base64 chars (+, /, =), and URL-safe chars (-, _, ., ~, %)
const cursorPattern = /^[A-Za-z0-9+/=\-_.~%]+$/
if (!cursorPattern.test(value)) {
logger.warn('Pagination cursor contains disallowed characters', {
paramName,
value: value.substring(0, 100),
})
return {
isValid: false,
error: `${paramName} contains invalid characters`,
}
}

return { isValid: true, sanitized: value }
}