Skip to content

Commit 5539c1b

Browse files
committed
fix(jsm): validate Assets workspaceId and honor last pagination flag
Address review findings on the Assets tools: - Add validateAssetsWorkspaceId and guard the workspaceId in every Assets route before it is interpolated into the API path (mirrors the existing cloudId guard) — prevents a crafted workspaceId from escaping the workspace-scoped path - Object schema list now falls back to the `last` flag when `isLast` is absent, so pagination doesn't stop early
1 parent 94c7430 commit 5539c1b

10 files changed

Lines changed: 103 additions & 10 deletions

File tree

apps/sim/app/api/tools/jsm/assets/attributes/route.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import { type NextRequest, NextResponse } from 'next/server'
44
import { jsmObjectTypeAttributesContract } from '@/lib/api/contracts/selectors/jsm'
55
import { parseRequest } from '@/lib/api/server'
66
import { checkInternalAuth } from '@/lib/auth/hybrid'
7-
import { validateJiraCloudId } from '@/lib/core/security/input-validation'
7+
import {
8+
validateAssetsWorkspaceId,
9+
validateJiraCloudId,
10+
} from '@/lib/core/security/input-validation'
811
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
912
import { parseAtlassianErrorMessage } from '@/tools/jira/utils'
1013
import { getAssetsApiBaseUrl, getJsmHeaders, resolveAssetsContext } from '@/tools/jsm/utils'
@@ -45,6 +48,11 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
4548
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
4649
}
4750

51+
const workspaceIdValidation = validateAssetsWorkspaceId(workspaceId, 'workspaceId')
52+
if (!workspaceIdValidation.isValid) {
53+
return NextResponse.json({ error: workspaceIdValidation.error }, { status: 400 })
54+
}
55+
4856
const query = new URLSearchParams()
4957
if (onlyValueEditable !== undefined) {
5058
query.append('onlyValueEditable', String(onlyValueEditable))

apps/sim/app/api/tools/jsm/assets/object-types/route.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import { type NextRequest, NextResponse } from 'next/server'
44
import { jsmListObjectTypesContract } from '@/lib/api/contracts/selectors/jsm'
55
import { parseRequest } from '@/lib/api/server'
66
import { checkInternalAuth } from '@/lib/auth/hybrid'
7-
import { validateJiraCloudId } from '@/lib/core/security/input-validation'
7+
import {
8+
validateAssetsWorkspaceId,
9+
validateJiraCloudId,
10+
} from '@/lib/core/security/input-validation'
811
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
912
import { parseAtlassianErrorMessage } from '@/tools/jira/utils'
1013
import { getAssetsApiBaseUrl, getJsmHeaders, resolveAssetsContext } from '@/tools/jsm/utils'
@@ -44,6 +47,11 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
4447
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
4548
}
4649

50+
const workspaceIdValidation = validateAssetsWorkspaceId(workspaceId, 'workspaceId')
51+
if (!workspaceIdValidation.isValid) {
52+
return NextResponse.json({ error: workspaceIdValidation.error }, { status: 400 })
53+
}
54+
4755
const query = new URLSearchParams()
4856
if (excludeAbstract !== undefined) query.append('excludeAbstract', String(excludeAbstract))
4957

apps/sim/app/api/tools/jsm/assets/object/create/route.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import { type NextRequest, NextResponse } from 'next/server'
44
import { jsmCreateObjectContract } from '@/lib/api/contracts/selectors/jsm'
55
import { parseRequest } from '@/lib/api/server'
66
import { checkInternalAuth } from '@/lib/auth/hybrid'
7-
import { validateJiraCloudId } from '@/lib/core/security/input-validation'
7+
import {
8+
validateAssetsWorkspaceId,
9+
validateJiraCloudId,
10+
} from '@/lib/core/security/input-validation'
811
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
912
import { parseAtlassianErrorMessage } from '@/tools/jira/utils'
1013
import {
@@ -49,6 +52,11 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
4952
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
5053
}
5154

55+
const workspaceIdValidation = validateAssetsWorkspaceId(workspaceId, 'workspaceId')
56+
if (!workspaceIdValidation.isValid) {
57+
return NextResponse.json({ error: workspaceIdValidation.error }, { status: 400 })
58+
}
59+
5260
const url = `${getAssetsApiBaseUrl(cloudId, workspaceId)}/object/create`
5361

5462
const response = await fetch(url, {

apps/sim/app/api/tools/jsm/assets/object/delete/route.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import { type NextRequest, NextResponse } from 'next/server'
44
import { jsmDeleteObjectContract } from '@/lib/api/contracts/selectors/jsm'
55
import { parseRequest } from '@/lib/api/server'
66
import { checkInternalAuth } from '@/lib/auth/hybrid'
7-
import { validateJiraCloudId } from '@/lib/core/security/input-validation'
7+
import {
8+
validateAssetsWorkspaceId,
9+
validateJiraCloudId,
10+
} from '@/lib/core/security/input-validation'
811
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
912
import { parseAtlassianErrorMessage } from '@/tools/jira/utils'
1013
import { getAssetsApiBaseUrl, getJsmHeaders, resolveAssetsContext } from '@/tools/jsm/utils'
@@ -43,6 +46,11 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
4346
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
4447
}
4548

49+
const workspaceIdValidation = validateAssetsWorkspaceId(workspaceId, 'workspaceId')
50+
if (!workspaceIdValidation.isValid) {
51+
return NextResponse.json({ error: workspaceIdValidation.error }, { status: 400 })
52+
}
53+
4654
const url = `${getAssetsApiBaseUrl(cloudId, workspaceId)}/object/${encodeURIComponent(objectId)}`
4755

4856
const response = await fetch(url, { method: 'DELETE', headers: getJsmHeaders(accessToken) })

apps/sim/app/api/tools/jsm/assets/object/get/route.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import { type NextRequest, NextResponse } from 'next/server'
44
import { jsmGetObjectContract } from '@/lib/api/contracts/selectors/jsm'
55
import { parseRequest } from '@/lib/api/server'
66
import { checkInternalAuth } from '@/lib/auth/hybrid'
7-
import { validateJiraCloudId } from '@/lib/core/security/input-validation'
7+
import {
8+
validateAssetsWorkspaceId,
9+
validateJiraCloudId,
10+
} from '@/lib/core/security/input-validation'
811
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
912
import { parseAtlassianErrorMessage } from '@/tools/jira/utils'
1013
import {
@@ -48,6 +51,11 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
4851
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
4952
}
5053

54+
const workspaceIdValidation = validateAssetsWorkspaceId(workspaceId, 'workspaceId')
55+
if (!workspaceIdValidation.isValid) {
56+
return NextResponse.json({ error: workspaceIdValidation.error }, { status: 400 })
57+
}
58+
5159
const url = `${getAssetsApiBaseUrl(cloudId, workspaceId)}/object/${encodeURIComponent(objectId)}`
5260

5361
const response = await fetch(url, { method: 'GET', headers: getJsmHeaders(accessToken) })

apps/sim/app/api/tools/jsm/assets/object/update/route.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import { type NextRequest, NextResponse } from 'next/server'
44
import { jsmUpdateObjectContract } from '@/lib/api/contracts/selectors/jsm'
55
import { parseRequest } from '@/lib/api/server'
66
import { checkInternalAuth } from '@/lib/auth/hybrid'
7-
import { validateJiraCloudId } from '@/lib/core/security/input-validation'
7+
import {
8+
validateAssetsWorkspaceId,
9+
validateJiraCloudId,
10+
} from '@/lib/core/security/input-validation'
811
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
912
import { parseAtlassianErrorMessage } from '@/tools/jira/utils'
1013
import {
@@ -50,6 +53,11 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
5053
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
5154
}
5255

56+
const workspaceIdValidation = validateAssetsWorkspaceId(workspaceId, 'workspaceId')
57+
if (!workspaceIdValidation.isValid) {
58+
return NextResponse.json({ error: workspaceIdValidation.error }, { status: 400 })
59+
}
60+
5361
const url = `${getAssetsApiBaseUrl(cloudId, workspaceId)}/object/${encodeURIComponent(objectId)}`
5462

5563
const body: Record<string, unknown> = { attributes }

apps/sim/app/api/tools/jsm/assets/schema/route.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import { type NextRequest, NextResponse } from 'next/server'
44
import { jsmGetObjectSchemaContract } from '@/lib/api/contracts/selectors/jsm'
55
import { parseRequest } from '@/lib/api/server'
66
import { checkInternalAuth } from '@/lib/auth/hybrid'
7-
import { validateJiraCloudId } from '@/lib/core/security/input-validation'
7+
import {
8+
validateAssetsWorkspaceId,
9+
validateJiraCloudId,
10+
} from '@/lib/core/security/input-validation'
811
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
912
import { parseAtlassianErrorMessage } from '@/tools/jira/utils'
1013
import { getAssetsApiBaseUrl, getJsmHeaders, resolveAssetsContext } from '@/tools/jsm/utils'
@@ -43,6 +46,11 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
4346
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
4447
}
4548

49+
const workspaceIdValidation = validateAssetsWorkspaceId(workspaceId, 'workspaceId')
50+
if (!workspaceIdValidation.isValid) {
51+
return NextResponse.json({ error: workspaceIdValidation.error }, { status: 400 })
52+
}
53+
4654
const url = `${getAssetsApiBaseUrl(cloudId, workspaceId)}/objectschema/${encodeURIComponent(schemaId)}`
4755

4856
const response = await fetch(url, { method: 'GET', headers: getJsmHeaders(accessToken) })

apps/sim/app/api/tools/jsm/assets/schemas/route.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import { type NextRequest, NextResponse } from 'next/server'
44
import { jsmListObjectSchemasContract } from '@/lib/api/contracts/selectors/jsm'
55
import { parseRequest } from '@/lib/api/server'
66
import { checkInternalAuth } from '@/lib/auth/hybrid'
7-
import { validateJiraCloudId } from '@/lib/core/security/input-validation'
7+
import {
8+
validateAssetsWorkspaceId,
9+
validateJiraCloudId,
10+
} from '@/lib/core/security/input-validation'
811
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
912
import { parseAtlassianErrorMessage } from '@/tools/jira/utils'
1013
import { getAssetsApiBaseUrl, getJsmHeaders, resolveAssetsContext } from '@/tools/jsm/utils'
@@ -45,6 +48,11 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
4548
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
4649
}
4750

51+
const workspaceIdValidation = validateAssetsWorkspaceId(workspaceId, 'workspaceId')
52+
if (!workspaceIdValidation.isValid) {
53+
return NextResponse.json({ error: workspaceIdValidation.error }, { status: 400 })
54+
}
55+
4856
const query = new URLSearchParams()
4957
if (startAt !== undefined) query.append('startAt', String(startAt))
5058
if (maxResults !== undefined) query.append('maxResults', String(maxResults))
@@ -76,7 +84,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
7684
ts: new Date().toISOString(),
7785
schemas: data.values ?? [],
7886
total: data.total ?? (data.values?.length || 0),
79-
isLast: data.isLast ?? true,
87+
isLast: data.isLast ?? data.last ?? true,
8088
},
8189
})
8290
} catch (error) {

apps/sim/app/api/tools/jsm/assets/search/route.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import { type NextRequest, NextResponse } from 'next/server'
44
import { jsmSearchObjectsAqlContract } from '@/lib/api/contracts/selectors/jsm'
55
import { parseRequest } from '@/lib/api/server'
66
import { checkInternalAuth } from '@/lib/auth/hybrid'
7-
import { validateJiraCloudId } from '@/lib/core/security/input-validation'
7+
import {
8+
validateAssetsWorkspaceId,
9+
validateJiraCloudId,
10+
} from '@/lib/core/security/input-validation'
811
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
912
import { parseAtlassianErrorMessage } from '@/tools/jira/utils'
1013
import {
@@ -60,6 +63,11 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
6063
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
6164
}
6265

66+
const workspaceIdValidation = validateAssetsWorkspaceId(workspaceId, 'workspaceId')
67+
if (!workspaceIdValidation.isValid) {
68+
return NextResponse.json({ error: workspaceIdValidation.error }, { status: 400 })
69+
}
70+
6371
const includeAttrs =
6472
includeAttributes === undefined ? true : String(includeAttributes) === 'true'
6573

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -627,6 +627,27 @@ export function validateJiraCloudId(
627627
})
628628
}
629629

630+
/**
631+
* Validates an Atlassian Assets workspace ID (a UUID-shaped, hyphenated
632+
* alphanumeric identifier) before it is interpolated into an API path.
633+
*
634+
* @param value - The Assets workspace ID to validate
635+
* @param paramName - Name of the parameter for error messages
636+
* @returns ValidationResult
637+
*/
638+
export function validateAssetsWorkspaceId(
639+
value: string | null | undefined,
640+
paramName = 'workspaceId'
641+
): ValidationResult {
642+
return validatePathSegment(value, {
643+
paramName,
644+
allowHyphens: true,
645+
allowUnderscores: false,
646+
allowDots: false,
647+
maxLength: 100,
648+
})
649+
}
650+
630651
/**
631652
* Validates Jira issue keys (format: PROJECT-123 or PROJECT-KEY-123)
632653
*

0 commit comments

Comments
 (0)