The problem
From the pull request: before this change, the Mailchimp Trigger node "accepted every POST to its webhook URL". Anyone who learned the URL could start workflow runs with made-up Mailchimp events. According to the pull request, Mailchimp now sends an X-Mailchimp-Signature header containing a timestamp and an HMAC-SHA256 of <timestamp>.<raw body>, keyed with a signing secret that Mailchimp returns once, in the response that creates the webhook.
The proposed fix
The node stores the secret when it creates the webhook and verifies every delivery with n8n's shared helper, which compares in constant time and rejects timestamps older than five minutes:
const hmac = createHmac('sha256', secret);
hmac.update(`${timestamp}.`);
hmac.update(payload);
return hmac.digest('hex');if (!verifySignature.call(this)) {
const res = this.getResponseObject();
res.status(401).send('Unauthorized').end();
return { noWebhookResponse: true };
}It signs the raw body rather than the parsed form body, which matters because Mailchimp posts form-encoded data and re-serializing it would not reproduce the original bytes. The tests cover a wrong secret, a changed body, a missing header, an expired timestamp and a missing raw body.
What I found in review
Keep every v1 signature
The header parser keeps only the last v1= value. In the t=...,v1=... scheme that Stripe uses, and that this header format mirrors, the header can carry one signature per active secret while a secret is being rotated, and the receiver should accept if any of them matches. With "last one wins", a valid first signature fails with 401. Collecting all v1 values and comparing each in constant time costs little; a test with t=...,v1=<bad>,v1=<good> would pin it.
The upgrade path is silent
The secret is captured in create. For a workflow that already has a Mailchimp webhook, checkExists returns true, so create never runs, no secret is stored, and verification is skipped because there is no secret to verify with. Existing workflows therefore stay unverified until someone deactivates and reactivates them, and nothing tells their owners. I suggested one of:
- log a one-time warning when a request arrives and no secret is stored, or
- make
checkExistsreturnfalsewhen a webhook id exists but the secret is missing, after deleting the old webhook so no duplicate is created; existing workflows then pick up signing on their next activation.
The documentation checkbox in the pull request was also unticked, and the "deactivate and activate to enable verification" step belongs there.
Say when it degrades
If the create response carries no signing_secret, the node falls back to accepting unsigned requests. A this.logger.warn(...) there would make that visible.
What happened next
Takeaways
- Signature schemes with a list of signatures exist for key rotation. A parser that keeps one value breaks rotation.
- When a provider shows a secret only once, every integration created before your code existed has no secret. Plan the upgrade path, or at least make the unprotected state visible.
- "Fail open when there is no secret" is a compatibility decision. It should be a logged one.