wacrm/src/lib/whatsapp/webhook-signature.ts
Arnas Donauskas be7edfd180
fix(webhook): require META_APP_SECRET, fail closed
The webhook signature verifier used to fall open with a warning log
when META_APP_SECRET was unset — a deliberate soft-rollout choice that
is unsafe for a public template. A forker who forgets the env var
would be running a fully spoofable webhook: anyone knowing the URL
could POST fabricated status updates and drift broadcast counts.

Now:
- `verifyMetaWebhookSignature` returns false when the secret is missing
  and logs at error level, so the route responds 401 to every POST
  until the operator configures it.
- Docs mark META_APP_SECRET as **required**:
  - docs/environment-variables.md lists it under Required, not
    Recommended.
  - docs/whatsapp-setup.md §6 is now "Signature verification (required)".
  - docs/troubleshooting.md updated accordingly.
- .env.local.example lists the var with guidance on where to find it.

Also fixes a pre-existing gitignore bug: `.env*` was swallowing
`.env.local.example`, which meant forkers running
`cp .env.local.example .env.local` from the getting-started guide
hit "no such file". Added a negation for `*.example`.

Upgrade note for existing deploys: set META_APP_SECRET (Meta → App
Settings → Basic → App Secret) before updating, or inbound webhooks
will 401 until it's configured.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 18:54:52 +03:00

48 lines
1.6 KiB
TypeScript

import crypto from 'node:crypto'
/**
* Verify the HMAC-SHA256 signature Meta attaches to webhook POSTs.
*
* Meta signs the raw request body with your App Secret and sends the
* result in the `x-hub-signature-256: sha256=<hex>` header. Without
* verification, anyone who knows our webhook URL can POST fabricated
* status updates and drift broadcast counts arbitrarily.
*
* Reference:
* https://developers.facebook.com/docs/graph-api/webhooks/getting-started#verify-payloads
*
* Contract:
* `META_APP_SECRET` is **required**. If it's missing we fail closed —
* every request is rejected until the operator configures the
* secret. A previous version fell open with a warning log, which is
* unsafe for a public template: anyone who forgets the env var would
* be running a fully spoofable webhook.
*/
export function verifyMetaWebhookSignature(
rawBody: string,
signatureHeader: string | null,
): boolean {
const secret = process.env.META_APP_SECRET
if (!secret) {
console.error(
'[webhook] META_APP_SECRET is not set — rejecting request. ' +
'Configure the env var (Meta → App Settings → Basic → App Secret) ' +
'to enable signature verification.',
)
return false
}
if (!signatureHeader) return false
if (!signatureHeader.startsWith('sha256=')) return false
const expected =
'sha256=' +
crypto.createHmac('sha256', secret).update(rawBody).digest('hex')
const a = Buffer.from(signatureHeader)
const b = Buffer.from(expected)
// Bail if lengths differ — timingSafeEqual throws otherwise.
if (a.length !== b.length) return false
return crypto.timingSafeEqual(a, b)
}