Skip to content

Commit 104a828

Browse files
waleedlatif1claude
andcommitted
fix(socket): add server-side lock validation for edges and subblocks
Defense-in-depth: adds lock checks to server-side handlers that were previously relying only on client-side validation. Edge operations (ADD, REMOVE, BATCH_ADD, BATCH_REMOVE): - Check if source or target blocks are protected before modifying edges Subblock updates: - Check if parent block is protected before updating subblock values Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 0cc0be7 commit 104a828

File tree

2 files changed

+237
-9
lines changed

2 files changed

+237
-9
lines changed

apps/sim/socket/database/operations.ts

Lines changed: 209 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1138,6 +1138,42 @@ async function handleEdgeOperationTx(tx: any, workflowId: string, operation: str
11381138
throw new Error('Missing required fields for add edge operation')
11391139
}
11401140

1141+
// Check if source or target blocks are protected (locked or inside locked parent)
1142+
const edgeBlocks = await tx
1143+
.select({
1144+
id: workflowBlocks.id,
1145+
locked: workflowBlocks.locked,
1146+
data: workflowBlocks.data,
1147+
})
1148+
.from(workflowBlocks)
1149+
.where(
1150+
and(
1151+
eq(workflowBlocks.workflowId, workflowId),
1152+
inArray(workflowBlocks.id, [payload.source, payload.target])
1153+
)
1154+
)
1155+
1156+
type EdgeBlockRecord = (typeof edgeBlocks)[number]
1157+
const blocksById: Record<string, EdgeBlockRecord> = Object.fromEntries(
1158+
edgeBlocks.map((b: EdgeBlockRecord) => [b.id, b])
1159+
)
1160+
1161+
const isBlockProtected = (blockId: string): boolean => {
1162+
const block = blocksById[blockId]
1163+
if (!block) return false
1164+
if (block.locked) return true
1165+
const parentId = (block.data as Record<string, unknown> | null)?.parentId as
1166+
| string
1167+
| undefined
1168+
if (parentId && blocksById[parentId]?.locked) return true
1169+
return false
1170+
}
1171+
1172+
if (isBlockProtected(payload.source) || isBlockProtected(payload.target)) {
1173+
logger.info(`Skipping edge add - source or target block is protected`)
1174+
break
1175+
}
1176+
11411177
await tx.insert(workflowEdges).values({
11421178
id: payload.id,
11431179
workflowId,
@@ -1156,15 +1192,63 @@ async function handleEdgeOperationTx(tx: any, workflowId: string, operation: str
11561192
throw new Error('Missing edge ID for remove operation')
11571193
}
11581194

1159-
const deleteResult = await tx
1160-
.delete(workflowEdges)
1195+
// Get the edge to check if connected blocks are protected
1196+
const [edgeToRemove] = await tx
1197+
.select({
1198+
sourceBlockId: workflowEdges.sourceBlockId,
1199+
targetBlockId: workflowEdges.targetBlockId,
1200+
})
1201+
.from(workflowEdges)
11611202
.where(and(eq(workflowEdges.id, payload.id), eq(workflowEdges.workflowId, workflowId)))
1162-
.returning({ id: workflowEdges.id })
1203+
.limit(1)
11631204

1164-
if (deleteResult.length === 0) {
1205+
if (!edgeToRemove) {
11651206
throw new Error(`Edge ${payload.id} not found in workflow ${workflowId}`)
11661207
}
11671208

1209+
// Check if source or target blocks are protected
1210+
const connectedBlocks = await tx
1211+
.select({
1212+
id: workflowBlocks.id,
1213+
locked: workflowBlocks.locked,
1214+
data: workflowBlocks.data,
1215+
})
1216+
.from(workflowBlocks)
1217+
.where(
1218+
and(
1219+
eq(workflowBlocks.workflowId, workflowId),
1220+
inArray(workflowBlocks.id, [edgeToRemove.sourceBlockId, edgeToRemove.targetBlockId])
1221+
)
1222+
)
1223+
1224+
type RemoveEdgeBlockRecord = (typeof connectedBlocks)[number]
1225+
const blocksById: Record<string, RemoveEdgeBlockRecord> = Object.fromEntries(
1226+
connectedBlocks.map((b: RemoveEdgeBlockRecord) => [b.id, b])
1227+
)
1228+
1229+
const isBlockProtected = (blockId: string): boolean => {
1230+
const block = blocksById[blockId]
1231+
if (!block) return false
1232+
if (block.locked) return true
1233+
const parentId = (block.data as Record<string, unknown> | null)?.parentId as
1234+
| string
1235+
| undefined
1236+
if (parentId && blocksById[parentId]?.locked) return true
1237+
return false
1238+
}
1239+
1240+
if (
1241+
isBlockProtected(edgeToRemove.sourceBlockId) ||
1242+
isBlockProtected(edgeToRemove.targetBlockId)
1243+
) {
1244+
logger.info(`Skipping edge remove - source or target block is protected`)
1245+
break
1246+
}
1247+
1248+
await tx
1249+
.delete(workflowEdges)
1250+
.where(and(eq(workflowEdges.id, payload.id), eq(workflowEdges.workflowId, workflowId)))
1251+
11681252
logger.debug(`Removed edge ${payload.id} from workflow ${workflowId}`)
11691253
break
11701254
}
@@ -1191,11 +1275,80 @@ async function handleEdgesOperationTx(
11911275

11921276
logger.info(`Batch removing ${ids.length} edges from workflow ${workflowId}`)
11931277

1278+
// Get edges to check connected blocks
1279+
const edgesToRemove = await tx
1280+
.select({
1281+
id: workflowEdges.id,
1282+
sourceBlockId: workflowEdges.sourceBlockId,
1283+
targetBlockId: workflowEdges.targetBlockId,
1284+
})
1285+
.from(workflowEdges)
1286+
.where(and(eq(workflowEdges.workflowId, workflowId), inArray(workflowEdges.id, ids)))
1287+
1288+
if (edgesToRemove.length === 0) {
1289+
logger.debug('No edges found to remove')
1290+
return
1291+
}
1292+
1293+
type EdgeToRemove = (typeof edgesToRemove)[number]
1294+
1295+
// Get all connected block IDs
1296+
const connectedBlockIds = new Set<string>()
1297+
edgesToRemove.forEach((e: EdgeToRemove) => {
1298+
connectedBlockIds.add(e.sourceBlockId)
1299+
connectedBlockIds.add(e.targetBlockId)
1300+
})
1301+
1302+
// Fetch blocks to check lock status
1303+
const connectedBlocks = await tx
1304+
.select({
1305+
id: workflowBlocks.id,
1306+
locked: workflowBlocks.locked,
1307+
data: workflowBlocks.data,
1308+
})
1309+
.from(workflowBlocks)
1310+
.where(
1311+
and(
1312+
eq(workflowBlocks.workflowId, workflowId),
1313+
inArray(workflowBlocks.id, Array.from(connectedBlockIds))
1314+
)
1315+
)
1316+
1317+
type EdgeBlockRecord = (typeof connectedBlocks)[number]
1318+
const blocksById: Record<string, EdgeBlockRecord> = Object.fromEntries(
1319+
connectedBlocks.map((b: EdgeBlockRecord) => [b.id, b])
1320+
)
1321+
1322+
const isBlockProtected = (blockId: string): boolean => {
1323+
const block = blocksById[blockId]
1324+
if (!block) return false
1325+
if (block.locked) return true
1326+
const parentId = (block.data as Record<string, unknown> | null)?.parentId as
1327+
| string
1328+
| undefined
1329+
if (parentId && blocksById[parentId]?.locked) return true
1330+
return false
1331+
}
1332+
1333+
const safeEdgeIds = edgesToRemove
1334+
.filter(
1335+
(e: EdgeToRemove) =>
1336+
!isBlockProtected(e.sourceBlockId) && !isBlockProtected(e.targetBlockId)
1337+
)
1338+
.map((e: EdgeToRemove) => e.id)
1339+
1340+
if (safeEdgeIds.length === 0) {
1341+
logger.info('All edges are connected to protected blocks, skipping removal')
1342+
return
1343+
}
1344+
11941345
await tx
11951346
.delete(workflowEdges)
1196-
.where(and(eq(workflowEdges.workflowId, workflowId), inArray(workflowEdges.id, ids)))
1347+
.where(
1348+
and(eq(workflowEdges.workflowId, workflowId), inArray(workflowEdges.id, safeEdgeIds))
1349+
)
11971350

1198-
logger.debug(`Batch removed ${ids.length} edges from workflow ${workflowId}`)
1351+
logger.debug(`Batch removed ${safeEdgeIds.length} edges from workflow ${workflowId}`)
11991352
break
12001353
}
12011354

@@ -1208,7 +1361,55 @@ async function handleEdgesOperationTx(
12081361

12091362
logger.info(`Batch adding ${edges.length} edges to workflow ${workflowId}`)
12101363

1211-
const edgeValues = edges.map((edge: Record<string, unknown>) => ({
1364+
// Get all connected block IDs to check lock status
1365+
const connectedBlockIds = new Set<string>()
1366+
edges.forEach((e: Record<string, unknown>) => {
1367+
connectedBlockIds.add(e.source as string)
1368+
connectedBlockIds.add(e.target as string)
1369+
})
1370+
1371+
// Fetch blocks to check lock status
1372+
const connectedBlocks = await tx
1373+
.select({
1374+
id: workflowBlocks.id,
1375+
locked: workflowBlocks.locked,
1376+
data: workflowBlocks.data,
1377+
})
1378+
.from(workflowBlocks)
1379+
.where(
1380+
and(
1381+
eq(workflowBlocks.workflowId, workflowId),
1382+
inArray(workflowBlocks.id, Array.from(connectedBlockIds))
1383+
)
1384+
)
1385+
1386+
type AddEdgeBlockRecord = (typeof connectedBlocks)[number]
1387+
const blocksById: Record<string, AddEdgeBlockRecord> = Object.fromEntries(
1388+
connectedBlocks.map((b: AddEdgeBlockRecord) => [b.id, b])
1389+
)
1390+
1391+
const isBlockProtected = (blockId: string): boolean => {
1392+
const block = blocksById[blockId]
1393+
if (!block) return false
1394+
if (block.locked) return true
1395+
const parentId = (block.data as Record<string, unknown> | null)?.parentId as
1396+
| string
1397+
| undefined
1398+
if (parentId && blocksById[parentId]?.locked) return true
1399+
return false
1400+
}
1401+
1402+
// Filter edges - only add edges where neither block is protected
1403+
const safeEdges = (edges as Array<Record<string, unknown>>).filter(
1404+
(e) => !isBlockProtected(e.source as string) && !isBlockProtected(e.target as string)
1405+
)
1406+
1407+
if (safeEdges.length === 0) {
1408+
logger.info('All edges connect to protected blocks, skipping add')
1409+
return
1410+
}
1411+
1412+
const edgeValues = safeEdges.map((edge: Record<string, unknown>) => ({
12121413
id: edge.id as string,
12131414
workflowId,
12141415
sourceBlockId: edge.source as string,
@@ -1219,7 +1420,7 @@ async function handleEdgesOperationTx(
12191420

12201421
await tx.insert(workflowEdges).values(edgeValues)
12211422

1222-
logger.debug(`Batch added ${edges.length} edges to workflow ${workflowId}`)
1423+
logger.debug(`Batch added ${safeEdges.length} edges to workflow ${workflowId}`)
12231424
break
12241425
}
12251426

apps/sim/socket/handlers/subblocks.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,11 @@ async function flushSubblockUpdate(
180180
let updateSuccessful = false
181181
await db.transaction(async (tx) => {
182182
const [block] = await tx
183-
.select({ subBlocks: workflowBlocks.subBlocks })
183+
.select({
184+
subBlocks: workflowBlocks.subBlocks,
185+
locked: workflowBlocks.locked,
186+
data: workflowBlocks.data,
187+
})
184188
.from(workflowBlocks)
185189
.where(and(eq(workflowBlocks.id, blockId), eq(workflowBlocks.workflowId, workflowId)))
186190
.limit(1)
@@ -189,6 +193,29 @@ async function flushSubblockUpdate(
189193
return
190194
}
191195

196+
// Check if block is locked directly
197+
if (block.locked) {
198+
logger.info(`Skipping subblock update - block ${blockId} is locked`)
199+
return
200+
}
201+
202+
// Check if block is inside a locked parent container
203+
const parentId = (block.data as Record<string, unknown> | null)?.parentId as
204+
| string
205+
| undefined
206+
if (parentId) {
207+
const [parentBlock] = await tx
208+
.select({ locked: workflowBlocks.locked })
209+
.from(workflowBlocks)
210+
.where(and(eq(workflowBlocks.id, parentId), eq(workflowBlocks.workflowId, workflowId)))
211+
.limit(1)
212+
213+
if (parentBlock?.locked) {
214+
logger.info(`Skipping subblock update - parent ${parentId} is locked`)
215+
return
216+
}
217+
}
218+
192219
const subBlocks = (block.subBlocks as any) || {}
193220
if (!subBlocks[subblockId]) {
194221
subBlocks[subblockId] = { id: subblockId, type: 'unknown', value }

0 commit comments

Comments
 (0)