mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-19 09:30:49 +08:00
fix(webhooks): fail closed when HMAC signing secret is not configured
Inbound webhook signature verification failed open for HMAC providers (GitHub, Intercom, Jira, JSM, Confluence, Cal.com, Notion, Greenhouse, Typeform, Fireflies, Circleback): when no signing secret was stored, verifyAuth returned null and the workflow executed on a fully attacker-controlled body. Reject these deliveries with 401 instead, matching the fail-closed Stripe/WhatsApp/Vercel providers. Run provider reachability/verification handshakes (Notion verification_token, Grain/Intercom ping) ahead of auth so the pre-secret setup handshake still completes — those return a canned 200 without executing the workflow, and real event payloads fall through to fail-closed verification. Update the trigger secret-field copy to state the secret is required for deliveries to be accepted (was misleadingly marked optional).
This commit is contained in:
@@ -111,6 +111,11 @@ async function handleWebhookPost(
|
||||
const responses: NextResponse[] = []
|
||||
|
||||
for (const { webhook: foundWebhook, workflow: foundWorkflow } of webhooksForPath) {
|
||||
const reachabilityResponse = handleProviderReachabilityTest(foundWebhook, body, requestId)
|
||||
if (reachabilityResponse) {
|
||||
return reachabilityResponse
|
||||
}
|
||||
|
||||
const authError = await verifyProviderAuth(
|
||||
foundWebhook,
|
||||
foundWorkflow,
|
||||
@@ -126,11 +131,6 @@ async function handleWebhookPost(
|
||||
return authError
|
||||
}
|
||||
|
||||
const reachabilityResponse = handleProviderReachabilityTest(foundWebhook, body, requestId)
|
||||
if (reachabilityResponse) {
|
||||
return reachabilityResponse
|
||||
}
|
||||
|
||||
const preprocessResult = await checkWebhookPreprocessing(foundWorkflow, foundWebhook, requestId)
|
||||
if (preprocessResult.error) {
|
||||
if (webhooksForPath.length > 1) {
|
||||
|
||||
@@ -48,7 +48,12 @@ export const githubHandler: WebhookProviderHandler = {
|
||||
verifyAuth({ request, rawBody, requestId, providerConfig }: AuthContext) {
|
||||
const secret = providerConfig.webhookSecret as string | undefined
|
||||
if (!secret) {
|
||||
return null
|
||||
logger.warn(
|
||||
`[${requestId}] GitHub webhook missing webhookSecret in providerConfig — rejecting request`
|
||||
)
|
||||
return new NextResponse('Unauthorized - GitHub signing secret not configured', {
|
||||
status: 401,
|
||||
})
|
||||
}
|
||||
|
||||
const signature =
|
||||
|
||||
@@ -49,7 +49,12 @@ export const intercomHandler: WebhookProviderHandler = {
|
||||
verifyAuth({ request, rawBody, requestId, providerConfig }: AuthContext) {
|
||||
const secret = providerConfig.webhookSecret as string | undefined
|
||||
if (!secret) {
|
||||
return null
|
||||
logger.warn(
|
||||
`[${requestId}] Intercom webhook missing webhookSecret in providerConfig — rejecting request`
|
||||
)
|
||||
return new NextResponse('Unauthorized - Intercom signing secret not configured', {
|
||||
status: 401,
|
||||
})
|
||||
}
|
||||
|
||||
const signature = request.headers.get('X-Hub-Signature')
|
||||
|
||||
@@ -16,6 +16,10 @@ interface HmacVerifierOptions {
|
||||
/**
|
||||
* Factory that creates a `verifyAuth` implementation for HMAC-signature-based providers.
|
||||
* Covers the common pattern: get secret → check header → validate signature → return 401 or null.
|
||||
*
|
||||
* Fails closed: when no signing secret is configured the request is rejected (401), matching
|
||||
* Stripe/WhatsApp/Vercel. A signed-provider webhook with no secret would otherwise accept any
|
||||
* unauthenticated body that knows the URL, downgrading the provider's mandatory signature check.
|
||||
*/
|
||||
export function createHmacVerifier({
|
||||
configKey,
|
||||
@@ -31,7 +35,12 @@ export function createHmacVerifier({
|
||||
}: AuthContext): Promise<NextResponse | null> => {
|
||||
const secret = providerConfig[configKey] as string | undefined
|
||||
if (!secret) {
|
||||
return null
|
||||
logger.warn(
|
||||
`[${requestId}] ${providerLabel} webhook missing signing secret in providerConfig — rejecting request`
|
||||
)
|
||||
return new NextResponse(`Unauthorized - ${providerLabel} signing secret not configured`, {
|
||||
status: 401,
|
||||
})
|
||||
}
|
||||
|
||||
const signature = request.headers.get(headerName)
|
||||
|
||||
@@ -38,8 +38,9 @@ export const circlebackWebhookTrigger: TriggerConfig = {
|
||||
id: 'webhookSecret',
|
||||
title: 'Signing Secret',
|
||||
type: 'short-input',
|
||||
placeholder: 'Paste signing secret from Circleback (optional)',
|
||||
description: 'Validates that webhook deliveries originate from Circleback using HMAC-SHA256.',
|
||||
placeholder: 'Paste signing secret from Circleback',
|
||||
description:
|
||||
'Validates that webhook deliveries originate from Circleback using HMAC-SHA256. Required: deliveries are rejected until this is set.',
|
||||
password: true,
|
||||
required: false,
|
||||
mode: 'trigger',
|
||||
|
||||
@@ -54,7 +54,7 @@ export function buildConfluenceExtraFields(triggerId: string): SubBlockConfig[]
|
||||
type: 'short-input',
|
||||
placeholder: 'Enter a strong secret',
|
||||
description:
|
||||
'Optional secret to validate webhook deliveries from Confluence using HMAC signature',
|
||||
'Secret to validate webhook deliveries from Confluence using HMAC signature. Required: deliveries are rejected until this is set.',
|
||||
password: true,
|
||||
required: false,
|
||||
mode: 'trigger',
|
||||
|
||||
@@ -25,7 +25,8 @@ export const firefliesTranscriptionCompleteTrigger: TriggerConfig = {
|
||||
title: 'Webhook Secret',
|
||||
type: 'short-input',
|
||||
placeholder: 'Enter your 16-32 character secret',
|
||||
description: 'Secret key for HMAC signature verification (set in Fireflies dashboard)',
|
||||
description:
|
||||
'Secret key for HMAC signature verification (set in Fireflies dashboard). Required: deliveries are rejected until this is set.',
|
||||
password: true,
|
||||
required: false,
|
||||
mode: 'trigger',
|
||||
|
||||
@@ -46,7 +46,8 @@ export const githubWebhookTrigger: TriggerConfig = {
|
||||
title: 'Webhook Secret',
|
||||
type: 'short-input',
|
||||
placeholder: 'Generate or enter a strong secret',
|
||||
description: 'Validates that webhook deliveries originate from GitHub.',
|
||||
description:
|
||||
'Validates that webhook deliveries originate from GitHub. Required: deliveries are rejected until this is set.',
|
||||
password: true,
|
||||
required: false,
|
||||
mode: 'trigger',
|
||||
|
||||
@@ -62,17 +62,17 @@ export function isGreenhouseEventMatch(triggerId: string, action: string): boole
|
||||
|
||||
/**
|
||||
* Builds extra fields for Greenhouse triggers.
|
||||
* Includes an optional secret key for HMAC signature verification.
|
||||
* Includes the secret key used for HMAC signature verification.
|
||||
*/
|
||||
export function buildGreenhouseExtraFields(triggerId: string): SubBlockConfig[] {
|
||||
return [
|
||||
{
|
||||
id: 'secretKey',
|
||||
title: 'Secret Key (Optional)',
|
||||
title: 'Secret Key',
|
||||
type: 'short-input',
|
||||
placeholder: 'Enter the same secret key configured in Greenhouse',
|
||||
description:
|
||||
'When set, requests must include a valid Signature header (HMAC-SHA256). If left empty, the endpoint does not verify signatures—only use on a private URL you fully control.',
|
||||
'Used to verify the HMAC-SHA256 Signature header. Required: deliveries are rejected until a secret key is configured here and in Greenhouse.',
|
||||
password: true,
|
||||
mode: 'trigger',
|
||||
condition: { field: 'selectedTriggerId', value: triggerId },
|
||||
@@ -101,7 +101,7 @@ export function greenhouseSetupInstructions(eventType: string): string {
|
||||
'In Greenhouse, go to <strong>Configure > Dev Center > Webhooks</strong>.',
|
||||
'Click <strong>Create New Webhook</strong>.',
|
||||
'Paste the Webhook URL into the <strong>Endpoint URL</strong> field.',
|
||||
'Enter a <strong>Secret Key</strong> for HMAC signature verification (recommended). Leave empty only if you accept unauthenticated POSTs to this URL.',
|
||||
'Enter a <strong>Secret Key</strong> for HMAC signature verification. This is required — deliveries without a valid signature are rejected.',
|
||||
`Under <strong>When</strong>, select the appropriate <strong>${eventType}</strong>.`,
|
||||
'Click <strong>Create Webhook</strong> to save.',
|
||||
'Click "Save" above to activate your trigger.',
|
||||
|
||||
@@ -34,7 +34,8 @@ export const jiraWebhookTrigger: TriggerConfig = {
|
||||
title: 'Webhook Secret',
|
||||
type: 'short-input',
|
||||
placeholder: 'Enter a strong secret',
|
||||
description: 'Optional secret to validate webhook deliveries from Jira using HMAC signature',
|
||||
description:
|
||||
'Secret to validate webhook deliveries from Jira using HMAC signature. Required: deliveries are rejected until this is set.',
|
||||
password: true,
|
||||
required: false,
|
||||
mode: 'trigger',
|
||||
|
||||
@@ -49,7 +49,8 @@ function jsmWebhookSecretField(triggerId: string): SubBlockConfig {
|
||||
title: 'Webhook Secret',
|
||||
type: 'short-input',
|
||||
placeholder: 'Enter a strong secret',
|
||||
description: 'Optional secret to validate webhook deliveries from Jira using HMAC signature',
|
||||
description:
|
||||
'Secret to validate webhook deliveries from Jira using HMAC signature. Required: deliveries are rejected until this is set.',
|
||||
password: true,
|
||||
required: false,
|
||||
mode: 'trigger',
|
||||
|
||||
@@ -45,9 +45,9 @@ export const typeformWebhookTrigger: TriggerConfig = {
|
||||
id: 'secret',
|
||||
title: 'Webhook Secret',
|
||||
type: 'short-input',
|
||||
placeholder: 'Enter a secret for webhook signature verification (optional)',
|
||||
placeholder: 'Enter a secret for webhook signature verification',
|
||||
description:
|
||||
'A secret string used to verify webhook authenticity. Highly recommended for security. Generate a secure random string (min 20 characters recommended).',
|
||||
'A secret string used to verify webhook authenticity. Required: deliveries are rejected until this is set. Generate a secure random string (min 20 characters recommended).',
|
||||
password: true,
|
||||
required: false,
|
||||
mode: 'trigger',
|
||||
|
||||
Reference in New Issue
Block a user