mirror of
https://github.com/Luchexx/wacrm.git
synced 2026-11-02 09:19:51 +00:00
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>
This commit is contained in:
parent
0b8377d2f1
commit
be7edfd180
16
.env.local.example
Normal file
16
.env.local.example
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# Supabase
|
||||||
|
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
|
||||||
|
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
|
||||||
|
|
||||||
|
# Supabase Service Role (for server-side operations like webhook handling)
|
||||||
|
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
|
||||||
|
|
||||||
|
# WhatsApp Token Encryption (64 hex chars = 32 bytes for AES-256-CBC)
|
||||||
|
# Generate with: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
|
||||||
|
ENCRYPTION_KEY=your-64-char-hex-key-here
|
||||||
|
|
||||||
|
# Meta App Secret — REQUIRED. Used to verify the HMAC-SHA256 signature
|
||||||
|
# Meta attaches to inbound webhook POSTs. Without it, the webhook
|
||||||
|
# rejects every request. Find it at:
|
||||||
|
# Meta for Developers → your app → App Settings → Basic → App Secret
|
||||||
|
META_APP_SECRET=your-meta-app-secret
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@ -32,6 +32,9 @@ yarn-error.log*
|
|||||||
|
|
||||||
# env files (can opt-in for committing if needed)
|
# env files (can opt-in for committing if needed)
|
||||||
.env*
|
.env*
|
||||||
|
# keep example files so forkers have a template to copy
|
||||||
|
!.env.local.example
|
||||||
|
!.env.example
|
||||||
|
|
||||||
# vercel
|
# vercel
|
||||||
.vercel
|
.vercel
|
||||||
|
|||||||
@ -12,12 +12,12 @@ minimal template; the table below is the full reference.
|
|||||||
| `NEXT_PUBLIC_SUPABASE_ANON_KEY` | Supabase → Project Settings → API → **anon / public** key | Public. Relies on RLS for safety. |
|
| `NEXT_PUBLIC_SUPABASE_ANON_KEY` | Supabase → Project Settings → API → **anon / public** key | Public. Relies on RLS for safety. |
|
||||||
| `SUPABASE_SERVICE_ROLE_KEY` | Supabase → Project Settings → API → **service_role** key | **Secret.** Bypasses RLS. Used by webhook + admin routes only. |
|
| `SUPABASE_SERVICE_ROLE_KEY` | Supabase → Project Settings → API → **service_role** key | **Secret.** Bypasses RLS. Used by webhook + admin routes only. |
|
||||||
| `ENCRYPTION_KEY` | Generate: `node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"` | 64 hex chars (32 bytes, AES-256-CBC). **Rotating breaks existing tokens.** |
|
| `ENCRYPTION_KEY` | Generate: `node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"` | 64 hex chars (32 bytes, AES-256-CBC). **Rotating breaks existing tokens.** |
|
||||||
|
| `META_APP_SECRET` | Meta → App Settings → Basic → **App Secret** | Verifies the `X-Hub-Signature-256` HMAC on every inbound webhook. **Without it the webhook rejects every request** — a public deploy cannot receive messages until this is set. |
|
||||||
|
|
||||||
## Recommended
|
## Recommended
|
||||||
|
|
||||||
| Variable | Purpose |
|
| Variable | Purpose |
|
||||||
| ---------------------- | ---------------------------------------------------------------------------------------- |
|
| ---------------------- | ---------------------------------------------------------------------------------------- |
|
||||||
| `META_APP_SECRET` | Meta → App Settings → Basic → **App Secret**. Enables `X-Hub-Signature-256` verification on incoming webhooks. |
|
|
||||||
| `NEXT_PUBLIC_SITE_URL` | Canonical public URL (e.g., `https://crm.example.com`). Used for absolute URLs, sitemap, OG images. |
|
| `NEXT_PUBLIC_SITE_URL` | Canonical public URL (e.g., `https://crm.example.com`). Used for absolute URLs, sitemap, OG images. |
|
||||||
|
|
||||||
## Optional
|
## Optional
|
||||||
@ -34,12 +34,14 @@ NEXT_PUBLIC_SUPABASE_URL=https://abcd1234.supabase.co
|
|||||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOi...
|
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOi...
|
||||||
SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOi...
|
SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOi...
|
||||||
|
|
||||||
|
# Meta App Secret — required for webhook signature verification
|
||||||
|
META_APP_SECRET=abcdef0123456789...
|
||||||
|
|
||||||
# Encryption — DO NOT change after first deploy
|
# Encryption — DO NOT change after first deploy
|
||||||
ENCRYPTION_KEY=3f9c0a7e4d8b2f1a6c5e8d4b9f0a2c6e8d4b9f0a2c6e8d4b9f0a2c6e8d4b9f0a
|
ENCRYPTION_KEY=3f9c0a7e4d8b2f1a6c5e8d4b9f0a2c6e8d4b9f0a2c6e8d4b9f0a2c6e8d4b9f0a
|
||||||
|
|
||||||
# Public URL + Meta webhook signing
|
# Public URL
|
||||||
NEXT_PUBLIC_SITE_URL=https://crm.example.com
|
NEXT_PUBLIC_SITE_URL=https://crm.example.com
|
||||||
META_APP_SECRET=abcdef0123456789...
|
|
||||||
|
|
||||||
# Automation cron
|
# Automation cron
|
||||||
AUTOMATION_CRON_SECRET=generate-a-long-random-string
|
AUTOMATION_CRON_SECRET=generate-a-long-random-string
|
||||||
|
|||||||
@ -50,9 +50,11 @@ Set `NEXT_PUBLIC_SITE_URL` **and** add the same origin to Supabase →
|
|||||||
- The callback URL must be reachable without auth. Test it yourself:
|
- The callback URL must be reachable without auth. Test it yourself:
|
||||||
`curl 'https://crm.example.com/api/whatsapp/webhook?hub.mode=subscribe&hub.verify_token=<token>&hub.challenge=test'`
|
`curl 'https://crm.example.com/api/whatsapp/webhook?hub.mode=subscribe&hub.verify_token=<token>&hub.challenge=test'`
|
||||||
should return `test`.
|
should return `test`.
|
||||||
- If you set `META_APP_SECRET`, check the app secret matches — a bad
|
- `META_APP_SECRET` must be set on the deployment (it's required —
|
||||||
secret causes 401s on inbound webhooks, but verification itself does
|
without it, every inbound POST gets a 401). If it's set but wrong,
|
||||||
not use it.
|
you'll also see 401s for every POST while the initial subscribe
|
||||||
|
handshake (which doesn't use it) works. Double-check the value
|
||||||
|
matches Meta → App Settings → Basic → App Secret exactly.
|
||||||
|
|
||||||
### Messages go out but nothing comes in
|
### Messages go out but nothing comes in
|
||||||
|
|
||||||
|
|||||||
@ -83,11 +83,14 @@ Under the webhook config, **Subscribe to** at minimum:
|
|||||||
`message_template_status_update` keeps Meta-approved template statuses in
|
`message_template_status_update` keeps Meta-approved template statuses in
|
||||||
sync on the broadcast UI.
|
sync on the broadcast UI.
|
||||||
|
|
||||||
## 6. Signature verification (recommended)
|
## 6. Signature verification (required)
|
||||||
|
|
||||||
Set `META_APP_SECRET` to your app's **App Secret** (Meta for Developers →
|
Set `META_APP_SECRET` to your app's **App Secret** (Meta for Developers →
|
||||||
App Settings → Basic). When present, WaCRM validates the `X-Hub-Signature-256`
|
App Settings → Basic). WaCRM validates the `X-Hub-Signature-256` HMAC
|
||||||
header on every inbound webhook and drops unsigned or forged calls.
|
on every inbound webhook and **rejects every request if the env var is
|
||||||
|
not set** — anyone who knew the webhook URL could otherwise spoof
|
||||||
|
messages and status updates. Configure it before pointing the webhook
|
||||||
|
at a deployed instance.
|
||||||
|
|
||||||
## 7. Send a test message
|
## 7. Send a test message
|
||||||
|
|
||||||
|
|||||||
@ -11,11 +11,12 @@ import crypto from 'node:crypto'
|
|||||||
* Reference:
|
* Reference:
|
||||||
* https://developers.facebook.com/docs/graph-api/webhooks/getting-started#verify-payloads
|
* https://developers.facebook.com/docs/graph-api/webhooks/getting-started#verify-payloads
|
||||||
*
|
*
|
||||||
* Rollout behavior:
|
* Contract:
|
||||||
* If `META_APP_SECRET` is unset, we **fail open** with a loud warning
|
* `META_APP_SECRET` is **required**. If it's missing we fail closed —
|
||||||
* log. This avoids breaking existing deployments at the moment this
|
* every request is rejected until the operator configures the
|
||||||
* code ships — operators can set the env var on their next deploy.
|
* secret. A previous version fell open with a warning log, which is
|
||||||
* Once the env var is set, verification is strict.
|
* unsafe for a public template: anyone who forgets the env var would
|
||||||
|
* be running a fully spoofable webhook.
|
||||||
*/
|
*/
|
||||||
export function verifyMetaWebhookSignature(
|
export function verifyMetaWebhookSignature(
|
||||||
rawBody: string,
|
rawBody: string,
|
||||||
@ -23,11 +24,12 @@ export function verifyMetaWebhookSignature(
|
|||||||
): boolean {
|
): boolean {
|
||||||
const secret = process.env.META_APP_SECRET
|
const secret = process.env.META_APP_SECRET
|
||||||
if (!secret) {
|
if (!secret) {
|
||||||
console.warn(
|
console.error(
|
||||||
'[webhook] META_APP_SECRET not set — skipping signature verification. ' +
|
'[webhook] META_APP_SECRET is not set — rejecting request. ' +
|
||||||
'Set the env var to harden against spoofed webhook POSTs.',
|
'Configure the env var (Meta → App Settings → Basic → App Secret) ' +
|
||||||
|
'to enable signature verification.',
|
||||||
)
|
)
|
||||||
return true
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!signatureHeader) return false
|
if (!signatureHeader) return false
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user