From 6e61fdbb07888c5f40f2dacf59c8083227d42961 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 13 Dec 2025 13:05:25 +0000 Subject: [PATCH] fix: prevent deleted webhooks from being accessed or updated Add isNull(deletedAt) filter to findById, updateLastUsed, updateToken, and softDelete methods in ChannelWebhookRepo. This ensures soft-deleted webhooks cannot be used to post messages, a security vulnerability that allowed deleted webhook tokens to remain functional indefinitely. --- .../src/repositories/channel-webhook-repo.ts | 44 +++++++++++++++++-- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/apps/backend/src/repositories/channel-webhook-repo.ts b/apps/backend/src/repositories/channel-webhook-repo.ts index f9f09e170..d730a8d79 100644 --- a/apps/backend/src/repositories/channel-webhook-repo.ts +++ b/apps/backend/src/repositories/channel-webhook-repo.ts @@ -19,6 +19,26 @@ export class ChannelWebhookRepo extends Effect.Service()("Ch ) const db = yield* Database.Database + // Override findById to filter by deletedAt = NULL (respect soft deletes) + const findById = (id: ChannelWebhookId, tx?: TxFn) => + db + .makeQuery( + (execute, data: { id: ChannelWebhookId }) => + execute((client) => + client + .select() + .from(schema.channelWebhooksTable) + .where( + and( + eq(schema.channelWebhooksTable.id, data.id), + isNull(schema.channelWebhooksTable.deletedAt), + ), + ) + .limit(1), + ).pipe(Effect.map((results) => Option.fromNullable(results[0]))), + policyRequire("ChannelWebhook", "select"), + )({ id }, tx) + // Find all webhooks for a channel const findByChannel = (channelId: ChannelId, tx?: TxFn) => db.makeQuery( @@ -66,7 +86,12 @@ export class ChannelWebhookRepo extends Effect.Service()("Ch client .update(schema.channelWebhooksTable) .set({ lastUsedAt: new Date(), updatedAt: new Date() }) - .where(eq(schema.channelWebhooksTable.id, data.id)) + .where( + and( + eq(schema.channelWebhooksTable.id, data.id), + isNull(schema.channelWebhooksTable.deletedAt), + ), + ) .returning(), ), policyRequire("ChannelWebhook", "update"), @@ -84,13 +109,18 @@ export class ChannelWebhookRepo extends Effect.Service()("Ch tokenSuffix: data.tokenSuffix, updatedAt: new Date(), }) - .where(eq(schema.channelWebhooksTable.id, data.id)) + .where( + and( + eq(schema.channelWebhooksTable.id, data.id), + isNull(schema.channelWebhooksTable.deletedAt), + ), + ) .returning(), ), policyRequire("ChannelWebhook", "update"), )({ id, tokenHash, tokenSuffix }, tx) - // Soft delete webhook + // Soft delete webhook (only if not already deleted) const softDelete = (id: ChannelWebhookId, tx?: TxFn) => db.makeQuery( (execute, data: { id: ChannelWebhookId }) => @@ -101,7 +131,12 @@ export class ChannelWebhookRepo extends Effect.Service()("Ch deletedAt: new Date(), updatedAt: new Date(), }) - .where(eq(schema.channelWebhooksTable.id, data.id)) + .where( + and( + eq(schema.channelWebhooksTable.id, data.id), + isNull(schema.channelWebhooksTable.deletedAt), + ), + ) .returning(), ), policyRequire("ChannelWebhook", "delete"), @@ -127,6 +162,7 @@ export class ChannelWebhookRepo extends Effect.Service()("Ch return { ...baseRepo, + findById, findByChannel, findByTokenHash, updateLastUsed,