fix(audit): webhook cluster — M21 (test-send isActive), M22 (cross-tenant dead-letter), L28 (ipv6 SSRF), L29 (rebind doc), L30 (replay event-time)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-02 12:40:41 +02:00
parent 29fb882478
commit 65ed90b603
3 changed files with 98 additions and 12 deletions

View File

@@ -309,10 +309,20 @@ export async function redeliverWebhookDelivery(
.limit(1);
if (!source) throw new NotFoundError('Delivery');
// L30: redeliver intentionally RE-SIGNS the original captured payload with a
// FRESH signature + fresh `X-Webhook-Timestamp` at dispatch time (see worker
// `finalPayload`). A receiver that judges freshness solely from the transport
// timestamp / delivery id would therefore accept arbitrarily-old event data
// as if it were new. This is by design (replaying a missed delivery), but the
// business-level event age must travel inside `data` so receivers can apply
// an event-time freshness check independent of the transport envelope. We
// surface the ORIGINAL delivery's `createdAt` as `original_event_at` (and keep
// the existing `retried_from` / `retried_at` replay markers).
const replayPayload = {
...(source.payload as Record<string, unknown>),
retried_from: deliveryId,
retried_at: new Date().toISOString(),
original_event_at: source.createdAt?.toISOString() ?? null,
};
const [next] = await db
@@ -363,6 +373,14 @@ export async function sendTestWebhook(portId: string, webhookId: string, eventTy
throw new NotFoundError('Webhook');
}
// M21: mirror redeliverWebhookDelivery — refuse to fire a live signed POST
// for a webhook an admin has explicitly disabled (e.g. because its endpoint
// was flagged). Without this, the test button is a convenient operator-
// controlled trigger for an otherwise-inert webhook.
if (!webhook.isActive) {
throw new NotFoundError('Webhook is inactive');
}
// Create a pending delivery record
const [delivery] = await db
.insert(webhookDeliveries)