Skip to content

fix(security): add webhook HMAC signature verification for Airtable, HubSpot, and Webflow#4550

Open
waleedlatif1 wants to merge 9 commits into
stagingfrom
fix/security-webhook-hmac
Open

fix(security): add webhook HMAC signature verification for Airtable, HubSpot, and Webflow#4550
waleedlatif1 wants to merge 9 commits into
stagingfrom
fix/security-webhook-hmac

Conversation

@waleedlatif1
Copy link
Copy Markdown
Collaborator

Summary

  • Airtable: implement verifyAuth using HMAC-SHA256 of the raw body keyed by the base64-decoded macSecretBase64 from providerConfig; compare against the hmac-sha256=<hex> prefix in X-Airtable-Content-MAC; fail-open with a warning when macSecretBase64 is absent (existing webhooks created before secret storage — no migration path since Airtable only returns the secret once at creation)
  • HubSpot: implement verifyAuth using SHA-256(clientSecret + raw body) compared against X-HubSpot-Signature per the v1 CRM webhook spec; clientSecret is already a required subBlock field in all HubSpot triggers so no existing webhooks break
  • Webflow: implement verifyAuth using HMAC-SHA256(secretKey, ${timestamp}:${rawBody}) compared against X-Webflow-Signature; store the secretKey from the webhook creation API response in providerConfig; add 5-minute replay protection via x-webflow-timestamp; fail-open when secretKey is absent for pre-existing webhooks
  • All three implementations use safeCompare from @sim/security/compare for constant-time comparison and fail-closed on missing secrets (except for migration backwards-compat cases which fail-open with a warning log)

Type of Change

  • Bug fix

Testing

Tested manually. Algorithms validated against official provider docs: Airtable webhooks overview, HubSpot webhook validation docs (v1 confirmed for CRM object subscriptions), Webflow Data API webhook docs. Airtable hmac-sha256= prefix confirmed from live header examples in community threads.

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

…HubSpot, and Webflow

- Airtable: verify X-Airtable-Content-MAC via HMAC-SHA256 of raw body; strip
  hmac-sha256= prefix before comparing; fail-open with warning when macSecretBase64
  is absent (existing webhooks predating secret storage)
- HubSpot: verify X-HubSpot-Signature via SHA-256(clientSecret + body) per v1 spec
- Webflow: verify X-Webflow-Signature via HMAC-SHA256(secretKey, timestamp:body);
  store secretKey from webhook creation response; add 5-min replay protection;
  fail-open when secretKey absent for pre-existing webhooks
- tool-schemas-v1.ts: linter formatting cleanup (computed keys)
@vercel
Copy link
Copy Markdown

vercel Bot commented May 10, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped May 11, 2026 6:21pm

Request Review

@cursor
Copy link
Copy Markdown

cursor Bot commented May 10, 2026

PR Summary

Medium Risk
Adds request authentication to multiple webhook providers; misconfiguration or signature mismatches can start rejecting legitimate webhooks, and Webflow introduces a timestamp-based replay window check.

Overview
Adds verifyAuth signature validation to Airtable, HubSpot, and Webflow webhook handlers, using constant-time safeCompare and returning 401 responses on missing/invalid signatures.

Persists provider secrets needed for verification (macSecretBase64 for Airtable and secretKey for Webflow) during subscription creation, and tightens error handling/logging via toError (including Webflow replay protection via x-webflow-timestamp).

Reviewed by Cursor Bugbot for commit ee3290a. Configure here.

Comment thread apps/sim/lib/webhooks/providers/airtable.ts Outdated
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 10, 2026

Greptile Summary

This PR adds HMAC webhook signature verification (verifyAuth) to the Airtable, HubSpot, and Webflow providers, using constant-time safeCompare throughout. All three implementations guard against missing secrets and delegate to the standard 401 path on failure, with Airtable deliberately failing-open for pre-existing webhooks that lack a stored macSecretBase64.

  • Airtable: HMAC-SHA256 of raw body keyed by base64-decoded macSecretBase64; fail-open when key is absent; stores secret from creation response. Also cleans up noisy CRITICAL_TRACE log prefixes and adopts toError().
  • HubSpot: SHA-256(clientSecret + raw body) against X-HubSpot-Signature (v1 CRM scheme); fail-closed when clientSecret is absent.
  • Webflow: HMAC-SHA256(secretKey, timestamp:rawBody) with 5-minute replay protection; stores secretKey from creation response, but falls back incorrectly to env.WEBFLOW_CLIENT_SECRET (an OAuth credential) instead of null when the API response omits the field — this would cause every HMAC check to fail with a mismatched key.

Confidence Score: 4/5

The Airtable and HubSpot implementations are correct and safe to merge. The Webflow implementation has one incorrect fallback that would silently store the wrong signing key, causing every Webflow webhook HMAC check to fail when the API response omits secretKey.

Airtable and HubSpot verification logic is sound and consistent. In the Webflow handler, responseBody.secretKey ?? env.WEBFLOW_CLIENT_SECRET ?? null stores the OAuth application credential as the HMAC signing key when Webflow's API does not return secretKey. Since Webflow signs payloads with the webhook-specific secret, not the OAuth credential, the HMAC comparison will always fail for those webhooks — silently breaking Webflow webhook delivery without any obvious error at subscription time.

apps/sim/lib/webhooks/providers/webflow.ts — specifically the secretKey fallback in createSubscription

Important Files Changed

Filename Overview
apps/sim/lib/webhooks/providers/airtable.ts Adds HMAC-SHA256 verifyAuth using the base64-decoded macSecretBase64; fails-open (returns null) for pre-existing webhooks that lack the secret; stores macSecretBase64 from creation response; other changes are comment cleanup and toError() adoption.
apps/sim/lib/webhooks/providers/hubspot.ts Adds verifyAuth using SHA-256(clientSecret + rawBody) against X-HubSpot-Signature (v1 scheme); fails-closed when clientSecret is absent; implementation is correct and uses safeCompare for constant-time comparison.
apps/sim/lib/webhooks/providers/webflow.ts Adds verifyAuth with HMAC-SHA256 and replay protection; has a P1 bug where env.WEBFLOW_CLIENT_SECRET (an OAuth credential) is used as a fallback webhook signing key instead of null, which would cause all HMAC checks to fail when Webflow does not return secretKey in the creation response.

Sequence Diagram

sequenceDiagram
    participant W as Webhook Source
    participant R as Webhook Route
    participant V as verifyAuth
    participant DB as providerConfig

    W->>R: POST /api/webhooks/trigger/:path
    R->>DB: Load providerConfig (macSecretBase64 / clientSecret / secretKey)
    R->>V: verifyAuth(request, rawBody, providerConfig)

    alt Airtable
        V->>V: macSecretBase64 absent? → return null (fail-open)
        V->>V: HMAC-SHA256(base64decode(macSecretBase64), rawBody)
        V->>V: safeCompare(computed, X-Airtable-Content-MAC)
    else HubSpot
        V->>V: clientSecret absent? → 401
        V->>V: SHA-256(clientSecret + rawBody)
        V->>V: safeCompare(computed, X-HubSpot-Signature)
    else Webflow
        V->>V: secretKey absent? → 401
        V->>V: Check x-webflow-timestamp replay window
        V->>V: HMAC-SHA256(secretKey, timestamp:rawBody)
        V->>V: safeCompare(computed, x-webflow-signature)
    end

    V-->>R: null (pass) or NextResponse(401) (reject)
    R->>W: 200 OK or 401 Unauthorized
Loading

Reviews (4): Last reviewed commit: "fix(webhooks): replace error as Error ca..." | Re-trigger Greptile

Comment thread apps/sim/lib/webhooks/providers/webflow.ts
Comment thread apps/sim/lib/webhooks/providers/hubspot.ts
… hmac encoding

- webflow: compare Date.now()/1000 against x-webflow-timestamp (both in Unix seconds) — the original Date.now() - ts comparison always exceeded the 300 000ms threshold, rejecting every valid webhook
- airtable: use utf8 encoding in HMAC update instead of ascii — ascii silently mangles bytes above 127, producing an incorrect digest for non-ASCII payloads
- hubspot: add TSDoc comment clarifying v1 signature scheme is intentional for CRM subscriptions
@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/lib/webhooks/providers/webflow.ts Outdated
… artifacts

- webflow: wrap HMAC computation and safeCompare in try-catch to return
  a clean 401 instead of an unhandled 500 when secretKey is a non-string
  truthy value (mirrors the defensive pattern already used by Airtable and
  HubSpot in the same PR)
- airtable: remove CRITICAL_TRACE/TRACE/DEBUG log prefixes, redundant
  "Error logging handled by logging session" comments, and other stale
  implementation notes that pre-date the refactor
…s milliseconds

Per official Webflow docs (example value 1722370035277, 13 digits), the header
is Unix milliseconds. Comparing Date.now()/1000 against a ms timestamp produced
a permanently-negative delta, making replay protection always pass. Reverted to
Date.now() - ts > 5 * 60 * 1000, matching Webflow's own reference implementation.
Also normalized caught error via toError().
@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 6ccc7b4. Configure here.

fix(security): store Webflow signing key at subscription creation time

- Remove all non-TSDoc inline comments from airtable, hubspot, and webflow providers
- Fix Webflow OAuth webhook signature verification gap: secretKey was never
  stored for OAuth-created webhooks (Webflow returns secretKey only for
  site-token webhooks). Now resolved at createSubscription time via
  responseBody.secretKey ?? env.WEBFLOW_CLIENT_SECRET ?? null, so verifyAuth
  has a single clean code path identical to Stripe and HubSpot
- Change Webflow verifyAuth from fail-open to fail-closed on missing secretKey
- Replace error as Error cast with toError() in Webflow createSubscription catch
- Add inline comment noting x-webflow-timestamp is Unix milliseconds
…viders

Replace all (error as Error) casts with toError() from @sim/utils/errors in
airtable and hubspot providers, per global standard in CLAUDE.md. Adds
toError import to airtable.ts.
@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/lib/webhooks/providers/webflow.ts
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit ee3290a. Configure here.

Comment thread apps/sim/lib/webhooks/providers/webflow.ts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant