Skip to content

Commit 5159d5c

Browse files
authored
verifies integration ownership before saving workflow (#98)
* verifies integration ownership before saving workflow * verify integration ownership when running workflow * fix bug
1 parent 052b4d5 commit 5159d5c

File tree

5 files changed

+140
-1
lines changed

5 files changed

+140
-1
lines changed

app/api/workflow/[workflowId]/execute/route.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { NextResponse } from "next/server";
33
import { start } from "workflow/api";
44
import { auth } from "@/lib/auth";
55
import { db } from "@/lib/db";
6+
import { validateWorkflowIntegrations } from "@/lib/db/integrations";
67
import { workflowExecutions, workflows } from "@/lib/db/schema";
78
import { executeWorkflow } from "@/lib/workflow-executor.workflow";
89
import type { WorkflowEdge, WorkflowNode } from "@/lib/workflow-store";
@@ -91,6 +92,25 @@ export async function POST(
9192
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
9293
}
9394

95+
// Validate that all integrationIds in workflow nodes belong to the current user
96+
const validation = await validateWorkflowIntegrations(
97+
workflow.nodes as WorkflowNode[],
98+
session.user.id
99+
);
100+
if (!validation.valid) {
101+
console.error(
102+
"[Workflow Execute] Invalid integration references:",
103+
validation.invalidIds
104+
);
105+
return NextResponse.json(
106+
{
107+
error: "Workflow contains invalid integration references",
108+
invalidIds: validation.invalidIds,
109+
},
110+
{ status: 403 }
111+
);
112+
}
113+
94114
// Parse request body
95115
const body = await request.json().catch(() => ({}));
96116
const input = body.input || {};

app/api/workflows/[workflowId]/route.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { and, eq } from "drizzle-orm";
22
import { NextResponse } from "next/server";
33
import { auth } from "@/lib/auth";
44
import { db } from "@/lib/db";
5+
import { validateWorkflowIntegrations } from "@/lib/db/integrations";
56
import { workflows } from "@/lib/db/schema";
67

78
export async function GET(
@@ -79,6 +80,24 @@ export async function PATCH(
7980
}
8081

8182
const body = await request.json();
83+
84+
// Validate that all integrationIds in nodes belong to the current user
85+
if (Array.isArray(body.nodes)) {
86+
const validation = await validateWorkflowIntegrations(
87+
body.nodes,
88+
session.user.id
89+
);
90+
if (!validation.valid) {
91+
return NextResponse.json(
92+
{
93+
error: "Invalid integration references in workflow",
94+
invalidIds: validation.invalidIds,
95+
},
96+
{ status: 403 }
97+
);
98+
}
99+
}
100+
82101
const updateData: Record<string, unknown> = {
83102
updatedAt: new Date(),
84103
};

app/api/workflows/[workflowId]/webhook/route.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { eq } from "drizzle-orm";
22
import { NextResponse } from "next/server";
33
import { start } from "workflow/api";
44
import { db } from "@/lib/db";
5+
import { validateWorkflowIntegrations } from "@/lib/db/integrations";
56
import { workflowExecutions, workflows } from "@/lib/db/schema";
67
import { executeWorkflow } from "@/lib/workflow-executor.workflow";
78
import type { WorkflowEdge, WorkflowNode } from "@/lib/workflow-store";
@@ -94,6 +95,22 @@ export async function POST(
9495
);
9596
}
9697

98+
// Validate that all integrationIds in workflow nodes belong to the workflow owner
99+
const validation = await validateWorkflowIntegrations(
100+
workflow.nodes as WorkflowNode[],
101+
workflow.userId
102+
);
103+
if (!validation.valid) {
104+
console.error(
105+
"[Webhook] Invalid integration references:",
106+
validation.invalidIds
107+
);
108+
return NextResponse.json(
109+
{ error: "Workflow contains invalid integration references" },
110+
{ status: 403, headers: corsHeaders }
111+
);
112+
}
113+
97114
// Parse request body
98115
const body = await request.json().catch(() => ({}));
99116

app/api/workflows/create/route.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { nanoid } from "nanoid";
33
import { NextResponse } from "next/server";
44
import { auth } from "@/lib/auth";
55
import { db } from "@/lib/db";
6+
import { validateWorkflowIntegrations } from "@/lib/db/integrations";
67
import { workflows } from "@/lib/db/schema";
78
import { generateId } from "@/lib/utils/id";
89

@@ -41,6 +42,21 @@ export async function POST(request: Request) {
4142
);
4243
}
4344

45+
// Validate that all integrationIds in nodes belong to the current user
46+
const validation = await validateWorkflowIntegrations(
47+
body.nodes,
48+
session.user.id
49+
);
50+
if (!validation.valid) {
51+
return NextResponse.json(
52+
{
53+
error: "Invalid integration references in workflow",
54+
invalidIds: validation.invalidIds,
55+
},
56+
{ status: 403 }
57+
);
58+
}
59+
4460
// Ensure there's always a trigger node (only add one if nodes array is empty)
4561
let nodes = body.nodes;
4662
if (nodes.length === 0) {

lib/db/integrations.ts

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import "server-only";
22

33
import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
4-
import { and, eq } from "drizzle-orm";
4+
import { and, eq, inArray } from "drizzle-orm";
55
import type { IntegrationConfig, IntegrationType } from "../types/integration";
66
import { db } from "./index";
77
import { integrations, type NewIntegration } from "./schema";
@@ -260,3 +260,70 @@ export async function deleteIntegration(
260260

261261
return result.length > 0;
262262
}
263+
264+
/**
265+
* Workflow node structure for validation
266+
*/
267+
type WorkflowNodeForValidation = {
268+
data?: {
269+
config?: {
270+
integrationId?: string;
271+
};
272+
};
273+
};
274+
275+
/**
276+
* Extract all integration IDs from workflow nodes
277+
*/
278+
export function extractIntegrationIds(
279+
nodes: WorkflowNodeForValidation[]
280+
): string[] {
281+
const integrationIds: string[] = [];
282+
283+
for (const node of nodes) {
284+
const integrationId = node.data?.config?.integrationId;
285+
if (integrationId && typeof integrationId === "string") {
286+
integrationIds.push(integrationId);
287+
}
288+
}
289+
290+
return [...new Set(integrationIds)];
291+
}
292+
293+
/**
294+
* Validate that all integration IDs in workflow nodes belong to the specified user.
295+
* This prevents users from accessing other users' credentials by embedding
296+
* foreign integration IDs in their workflows.
297+
*
298+
* @returns Object with `valid` boolean and optional `invalidIds` array
299+
*/
300+
export async function validateWorkflowIntegrations(
301+
nodes: WorkflowNodeForValidation[],
302+
userId: string
303+
): Promise<{ valid: boolean; invalidIds?: string[] }> {
304+
const integrationIds = extractIntegrationIds(nodes);
305+
306+
if (integrationIds.length === 0) {
307+
return { valid: true };
308+
}
309+
310+
// Query for integrations that belong to this user
311+
const userIntegrations = await db
312+
.select({ id: integrations.id })
313+
.from(integrations)
314+
.where(
315+
and(
316+
inArray(integrations.id, integrationIds),
317+
eq(integrations.userId, userId)
318+
)
319+
);
320+
321+
const validIds = new Set(userIntegrations.map((i) => i.id));
322+
const invalidIds = integrationIds.filter((id) => !validIds.has(id));
323+
324+
if (invalidIds.length > 0) {
325+
return { valid: false, invalidIds };
326+
}
327+
328+
return { valid: true };
329+
}

0 commit comments

Comments
 (0)