mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
fix(confluence): add input validation for SSRF-flagged parameters (#3351)
This commit is contained in:
@@ -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')
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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')
|
||||
@@ -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}`)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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')
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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')
|
||||
@@ -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}`)
|
||||
|
||||
@@ -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()}`
|
||||
|
||||
|
||||
@@ -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')
|
||||
@@ -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()}`
|
||||
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user