diff --git a/packages/services/service-messaging/src/http-outbox-parked-record.integration.test.ts b/packages/services/service-messaging/src/http-outbox-parked-record.integration.test.ts index 76cbcb923f2..824a65d2930 100644 --- a/packages/services/service-messaging/src/http-outbox-parked-record.integration.test.ts +++ b/packages/services/service-messaging/src/http-outbox-parked-record.integration.test.ts @@ -11,7 +11,9 @@ * nothing. The obvious fix (write the discarded event as a `dead` row) is * *actively unsafe on its own*: `redeliver()` reset ANY terminal row to * `pending` with no signature check, behind `POST /api/v1/webhooks/redeliver` - * which any authenticated user can reach. A parked row has no signature — + * (admitted to a caller with edit access to `sys_webhook`, as decided by + * `packages/plugins/plugin-webhooks/src/redeliver-entitlement.ts`). A parked + * row has no signature — * the signature is computed at enqueue from the very secret that could not be * resolved — so redelivering one would put the payload on the wire UNSIGNED, * reopening #7799 through a door nobody would think to audit. diff --git a/packages/services/service-messaging/src/http-outbox.ts b/packages/services/service-messaging/src/http-outbox.ts index 7c03a1ea7d0..d0d2aed0939 100644 --- a/packages/services/service-messaging/src/http-outbox.ts +++ b/packages/services/service-messaging/src/http-outbox.ts @@ -487,8 +487,10 @@ export class HttpRedeliverError extends Error { * ## Why this is a security refusal and not a tidiness one * A parked row has no {@link HttpDelivery.signature}, because the signature is * computed at enqueue from the very secret that could not be resolved (#7722, - * #7799). `POST /api/v1/webhooks/redeliver` is reachable by any authenticated - * user. Without this refusal, making the drop durable would hand that user a + * #7799). `POST /api/v1/webhooks/redeliver` admits a caller with edit + * (`update`) access to `sys_webhook`, as decided by + * `packages/plugins/plugin-webhooks/src/redeliver-entitlement.ts`. Without this + * refusal, making the drop durable would hand any such webhook administrator a * button that delivers the webhook UNSIGNED — reopening #7799 through a door * nobody would think to audit. **An unsigned redelivery is strictly worse than * an unrecorded drop**, which is why this guard lands with — and logically diff --git a/packages/services/service-messaging/src/messaging-service.ts b/packages/services/service-messaging/src/messaging-service.ts index 40f99429280..c48be0bdccc 100644 --- a/packages/services/service-messaging/src/messaging-service.ts +++ b/packages/services/service-messaging/src/messaging-service.ts @@ -387,8 +387,10 @@ export class MessagingService { * * [#10740] `options.tenantId` is the REQUESTING caller's organization, and * it is the whole reason this method takes a second argument: the door in - * front of it (`POST /api/v1/webhooks/redeliver`) is open to any - * authenticated user, and `sys_http_delivery` is tenant-scoped. The + * front of it (`POST /api/v1/webhooks/redeliver`) admits any caller with + * edit (`update`) access to `sys_webhook`, as decided by + * `packages/plugins/plugin-webhooks/src/redeliver-entitlement.ts` — a caller + * in any organization — and `sys_http_delivery` is tenant-scoped. The * outbox applies it to the rows it reads and the row it writes, so a * caller can only replay deliveries in its own organization; a row * elsewhere is `RESOURCE_NOT_FOUND`. ⛔ There is no tenant-audit bypass