Skip to content

Commit 3c6cdd5

Browse files
committed
fix(triggers): fail closed on missing webhook secret and clean up Zendesk orphans
Address review feedback on the auto-registration changes: - verifyAuth now rejects (401) when webhookSecret is absent for GitLab, PagerDuty, and Zendesk. Since the secret is generated/fetched during auto-registration and stored before the webhook can receive deliveries, a missing secret indicates misconfiguration and must fail closed rather than skip signature verification. Adds an opt-in requireSecret flag to createHmacVerifier (default off, preserving behavior for other providers). - Zendesk createSubscription now deletes the just-created webhook if the follow-up signing-secret fetch fails, avoiding an orphaned subscription in Zendesk when setup cannot complete.
1 parent 5aa6d78 commit 3c6cdd5

4 files changed

Lines changed: 42 additions & 4 deletions

File tree

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,14 @@ function gitlabProjectHooksUrl(projectId: string): string {
2929
export const gitlabHandler: WebhookProviderHandler = {
3030
/**
3131
* GitLab echoes the configured "Secret token" verbatim in the `X-Gitlab-Token`
32-
* header (plain equality, not an HMAC). Skip verification when no token is set.
32+
* header (plain equality, not an HMAC). The secret is generated during
33+
* auto-registration, so a missing secret means misconfiguration — fail closed.
3334
*/
3435
verifyAuth({ request, requestId, providerConfig }: AuthContext) {
3536
const secret = providerConfig.webhookSecret as string | undefined
3637
if (!secret) {
37-
return null
38+
logger.warn(`[${requestId}] GitLab webhook secret not configured`)
39+
return new NextResponse('Unauthorized - Missing GitLab webhook secret', { status: 401 })
3840
}
3941

4042
const token = request.headers.get('X-Gitlab-Token')

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ export const pagerdutyHandler: WebhookProviderHandler = {
6060
headerName: 'X-PagerDuty-Signature',
6161
validateFn: validatePagerDutySignature,
6262
providerLabel: 'PagerDuty',
63+
// The signing secret is captured during auto-registration, so a missing
64+
// secret means misconfiguration — fail closed rather than skip verification.
65+
requireSecret: true,
6366
}),
6467

6568
async matchEvent({ body, requestId, providerConfig }: EventMatchContext) {

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ interface HmacVerifierOptions {
1111
headerName: string
1212
validateFn: (secret: string, signature: string, rawBody: string) => boolean | Promise<boolean>
1313
providerLabel: string
14+
/**
15+
* When true, reject (401) if no secret is configured instead of skipping
16+
* verification. Use for providers where the secret is always present (e.g.
17+
* auto-registered webhooks) so a missing secret fails closed.
18+
*/
19+
requireSecret?: boolean
1420
}
1521

1622
/**
@@ -22,6 +28,7 @@ export function createHmacVerifier({
2228
headerName,
2329
validateFn,
2430
providerLabel,
31+
requireSecret = false,
2532
}: HmacVerifierOptions) {
2633
return async ({
2734
request,
@@ -31,6 +38,12 @@ export function createHmacVerifier({
3138
}: AuthContext): Promise<NextResponse | null> => {
3239
const secret = providerConfig[configKey] as string | undefined
3340
if (!secret) {
41+
if (requireSecret) {
42+
logger.warn(`[${requestId}] ${providerLabel} webhook secret not configured`)
43+
return new NextResponse(`Unauthorized - Missing ${providerLabel} webhook secret`, {
44+
status: 401,
45+
})
46+
}
3447
return null
3548
}
3649

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

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,18 @@ function zendeskAuthHeader(email: string, apiToken: string): string {
3030
return `Basic ${Buffer.from(`${email}/token:${apiToken}`).toString('base64')}`
3131
}
3232

33+
/** Best-effort delete used to avoid orphaning a webhook when post-create setup fails. */
34+
async function deleteZendeskWebhookQuietly(
35+
apiBase: string,
36+
authHeader: string,
37+
webhookId: string
38+
): Promise<void> {
39+
await fetch(`${apiBase}/webhooks/${webhookId}`, {
40+
method: 'DELETE',
41+
headers: { Authorization: authHeader },
42+
}).catch(() => {})
43+
}
44+
3345
/** Maximum allowed clock skew (5 minutes) between Zendesk's signed timestamp and now, per Zendesk docs. */
3446
const ZENDESK_TIMESTAMP_MAX_SKEW_MS = 5 * 60 * 1000
3547

@@ -67,7 +79,10 @@ export const zendeskHandler: WebhookProviderHandler = {
6779
verifyAuth({ request, rawBody, requestId, providerConfig }: AuthContext) {
6880
const secret = providerConfig.webhookSecret as string | undefined
6981
if (!secret) {
70-
return null
82+
// The signing secret is fetched during auto-registration, so a missing
83+
// secret means misconfiguration — fail closed rather than skip.
84+
logger.warn(`[${requestId}] Zendesk webhook secret not configured`)
85+
return new NextResponse('Unauthorized - Missing Zendesk webhook secret', { status: 401 })
7186
}
7287

7388
const signature = request.headers.get('X-Zendesk-Webhook-Signature')
@@ -201,12 +216,17 @@ export const zendeskHandler: WebhookProviderHandler = {
201216
`[${ctx.requestId}] Created Zendesk webhook ${externalId} but failed to fetch signing secret (${secretRes.status})`,
202217
{ detail }
203218
)
219+
// Avoid leaving an orphaned webhook in Zendesk when secret retrieval fails.
220+
await deleteZendeskWebhookQuietly(apiBase, authHeader, externalId)
204221
throw new Error(`Failed to fetch Zendesk signing secret: ${secretRes.status}`)
205222
}
206223

207224
const secretBody = asRecord((await secretRes.json().catch(() => ({}))) as unknown)
208225
const secret = asRecord(secretBody.signing_secret).secret as string | undefined
209-
if (!secret) throw new Error('Zendesk did not return a signing secret for the webhook.')
226+
if (!secret) {
227+
await deleteZendeskWebhookQuietly(apiBase, authHeader, externalId)
228+
throw new Error('Zendesk did not return a signing secret for the webhook.')
229+
}
210230

211231
logger.info(`[${ctx.requestId}] Created Zendesk webhook ${externalId}`)
212232
return { providerConfigUpdates: { externalId, webhookSecret: secret } }

0 commit comments

Comments
 (0)