Skip to content

Commit c2e7e01

Browse files
committed
fix(triggers): scope webhook secrets to owner and add Zendesk replay protection
Address review feedback: - Add paramVisibility: 'user-only' to the webhookSecret fields for GitLab, PagerDuty, and Zendesk so signing secrets are scoped to the credential owner and not exposed to workspace collaborators (repo convention). - Reject Zendesk deliveries whose signed timestamp is more than 5 minutes from now, closing a replay window once an event id ages out of the idempotency cache. The X-Zendesk-Webhook-Signature-Timestamp header is ISO-8601, so it is parsed with Date.parse (matches the Slack handler's skew-check convention).
1 parent fd4de8d commit c2e7e01

4 files changed

Lines changed: 24 additions & 0 deletions

File tree

apps/sim/lib/webhooks/providers/zendesk.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,20 @@ function asRecord(value: unknown): Record<string, unknown> {
1616
return (value as Record<string, unknown>) || {}
1717
}
1818

19+
/** Maximum allowed clock skew (5 minutes) between Zendesk's signed timestamp and now, per Zendesk docs. */
20+
const ZENDESK_TIMESTAMP_MAX_SKEW_MS = 5 * 60 * 1000
21+
22+
/**
23+
* Verify the signed timestamp is recent to prevent replay of captured deliveries.
24+
* Zendesk sends `X-Zendesk-Webhook-Signature-Timestamp` as an ISO-8601 string
25+
* (e.g. `2025-01-24T15:30:00.000Z`), so it is parsed with `Date.parse`.
26+
*/
27+
function isZendeskTimestampFresh(timestamp: string): boolean {
28+
const signedAt = Date.parse(timestamp)
29+
if (Number.isNaN(signedAt)) return false
30+
return Math.abs(Date.now() - signedAt) <= ZENDESK_TIMESTAMP_MAX_SKEW_MS
31+
}
32+
1933
/**
2034
* Zendesk signs `timestamp + rawBody` (no separator) with HMAC-SHA256 keyed by
2135
* the webhook's signing secret, then base64-encodes it into
@@ -49,6 +63,13 @@ export const zendeskHandler: WebhookProviderHandler = {
4963
return new NextResponse('Unauthorized - Missing Zendesk signature', { status: 401 })
5064
}
5165

66+
if (!isZendeskTimestampFresh(timestamp)) {
67+
logger.warn(`[${requestId}] Zendesk webhook timestamp outside the allowed window`, {
68+
timestamp,
69+
})
70+
return new NextResponse('Unauthorized - Stale Zendesk timestamp', { status: 401 })
71+
}
72+
5273
if (!validateZendeskSignature(secret, signature, timestamp, rawBody)) {
5374
logger.warn(`[${requestId}] Zendesk signature verification failed`)
5475
return new NextResponse('Unauthorized - Invalid Zendesk signature', { status: 401 })

apps/sim/triggers/gitlab/utils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ export function buildGitLabExtraFields(triggerId: string): SubBlockConfig[] {
5959
placeholder: 'Generate or enter a strong secret token',
6060
description: 'Validates that webhook deliveries originate from GitLab (X-Gitlab-Token).',
6161
password: true,
62+
paramVisibility: 'user-only',
6263
required: false,
6364
mode: 'trigger',
6465
condition: { field: 'selectedTriggerId', value: triggerId },

apps/sim/triggers/pagerduty/utils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ export function buildPagerDutyExtraFields(triggerId: string): SubBlockConfig[] {
5757
description:
5858
'Validates that webhook deliveries originate from PagerDuty (X-PagerDuty-Signature).',
5959
password: true,
60+
paramVisibility: 'user-only',
6061
required: false,
6162
mode: 'trigger',
6263
condition: { field: 'selectedTriggerId', value: triggerId },

apps/sim/triggers/zendesk/utils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export function buildZendeskExtraFields(triggerId: string): SubBlockConfig[] {
5555
description:
5656
'Validates that webhook deliveries originate from Zendesk (X-Zendesk-Webhook-Signature).',
5757
password: true,
58+
paramVisibility: 'user-only',
5859
required: false,
5960
mode: 'trigger',
6061
condition: { field: 'selectedTriggerId', value: triggerId },

0 commit comments

Comments
 (0)