Skip to content

Commit 592e048

Browse files
waleedlatif1claude
andcommitted
fix(security): scope webhook deploy path conflict to active webhooks
findConflictingWebhookPathOwner omitted the isActive filter that the runtime dispatcher (findAllWebhooksForPath) applies, so an inactive but non-archived webhook from another workflow (e.g. after undeploy or failure auto-disable) would permanently block any new deployment on that path even though it never receives deliveries. Align the guard with the runtime isActive + archivedAt filter; the earliest-owner runtime check remains the authoritative cross-tenant protection. Also trims verbose TSDoc on the webhook path-isolation helpers. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 474d73f commit 592e048

2 files changed

Lines changed: 12 additions & 17 deletions

File tree

apps/sim/lib/webhooks/deploy.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,12 @@ const logger = createLogger('DeployWebhookSync')
2424
const CREDENTIAL_SET_PREFIX = 'credentialSet:'
2525

2626
/**
27-
* Returns the id of a workflow that already owns an active (non-archived)
28-
* webhook on the given path, when that workflow is different from the one being
29-
* deployed. Webhook paths are user-controlled and the database only enforces
30-
* uniqueness per deployment version, so without this guard two tenants could
31-
* register the same public path and a delivery to one would fan out to the
32-
* other. Returns `null` when the path is free or only used by this workflow.
27+
* Returns the id of a different workflow that already owns an active webhook on
28+
* the given path, or `null` if the path is free or owned by this workflow.
29+
* Guards against cross-tenant path collisions, since paths are user-controlled
30+
* and only unique per deployment version. Mirrors the runtime dispatcher's
31+
* `isActive`/`archivedAt` filter so inactive (e.g. undeployed) webhooks — which
32+
* never receive deliveries — don't permanently reserve a path.
3333
*/
3434
async function findConflictingWebhookPathOwner(params: {
3535
path: string
@@ -40,7 +40,7 @@ async function findConflictingWebhookPathOwner(params: {
4040
const existing = await db
4141
.select({ workflowId: webhook.workflowId })
4242
.from(webhook)
43-
.where(and(eq(webhook.path, path), isNull(webhook.archivedAt)))
43+
.where(and(eq(webhook.path, path), eq(webhook.isActive, true), isNull(webhook.archivedAt)))
4444

4545
const conflict = existing.find((row) => row.workflowId !== workflowId)
4646
return conflict ? conflict.workflowId : null

apps/sim/lib/webhooks/processor.ts

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -305,17 +305,12 @@ async function findWebhookAndWorkflow(
305305
}
306306

307307
/**
308-
* Find ALL webhooks matching a path.
308+
* Finds all webhooks matching a path, scoped to a single workflow.
309309
*
310-
* Used for credential sets where multiple webhooks legitimately share the same
311-
* path. Legitimate fan-out is always scoped to a single workflow (credential
312-
* set sync only ever creates rows for one workflow), but webhook paths are
313-
* user-controlled and only unique per deployment version, so two different
314-
* workflows (tenants) can register the same public path. Dispatching a delivery
315-
* to webhooks across multiple workflows would let one tenant receive and process
316-
* another tenant's signed webhook payloads. To prevent that cross-tenant
317-
* collision we constrain the returned rows to the workflow that registered the
318-
* path first and drop any foreign rows.
310+
* Legitimate fan-out (credential sets) is always within one workflow, but paths
311+
* are user-controlled and only unique per deployment version, so two tenants can
312+
* register the same path. On collision we keep only the workflow that registered
313+
* the path first, so one tenant can never receive another's webhook deliveries.
319314
*/
320315
export async function findAllWebhooksForPath(
321316
options: WebhookProcessorOptions

0 commit comments

Comments
 (0)