Is your feature request related to a problem? Please describe.
The Chatwoot app's inbound route POST /webhooks/chatwoot/{session}/{appId} accepts any request that knows the app id. It takes the recipient and the message content (including attachment data_url) from the posted body and sends from the paired WhatsApp number. It does not check who sent the request.
The app id alone is weak protection:
- It appears in the Chatwoot API inbox's webhook URL, which any Chatwoot user with inbox settings access can read.
- It appears in WAHA's logs.
So any process or container that can reach WAHA and has seen the app id can send any message to any recipient from the paired number. It bypasses Chatwoot entirely. Putting WAHA behind a network boundary helps, but does not remove the risk from anything inside that boundary.
Describe the solution you'd like
Chatwoot already signs API-inbox webhooks. From v4.x each Channel::Api has a secret, and every inbox webhook delivery carries:
X-Chatwoot-Timestamp: <unix seconds>
X-Chatwoot-Signature: sha256=<hex HMAC-SHA256(secret, "<timestamp>.<raw body>")>
(See lib/webhooks/trigger.rb#request_headers, and app/listeners/webhook_listener.rb, which passes inbox.channel.secret.)
Proposal:
- An optional
config.webhookSecret field on the Chatwoot app (the inbox's secret from Chatwoot).
- When it is set, the controller computes the HMAC over the raw request body. It rejects the request with 401 when the signature is missing or does not match (constant-time compare), or when the timestamp is outside a small window (for example ±5 minutes) to stop replay.
- When the field is not set, today's behaviour stays unchanged, so nothing breaks for existing users. A startup warning that the route is unauthenticated would help.
Describe alternatives you've considered
- An unguessable app id. It is still readable in the Chatwoot UI and in logs, so it is not a real secret.
- Network isolation or a reverse proxy that checks the HMAC. This works, but everyone has to rebuild it outside WAHA. The proxy also needs the raw body and the secret, so it belongs in the app.
- WAHA's existing webhook
hmac.key. This signs WAHA → receiver traffic only. It does not authenticate Chatwoot → WAHA.
Additional context
Seen on WAHA 2026.9.1 (GOWS) with Chatwoot v4.18.0, both self-hosted. grep -ri x-chatwoot-signature /app/dist in the WAHA image returns nothing. Happy to test a build.
Is your feature request related to a problem? Please describe.
The Chatwoot app's inbound route
POST /webhooks/chatwoot/{session}/{appId}accepts any request that knows the app id. It takes the recipient and the message content (including attachmentdata_url) from the posted body and sends from the paired WhatsApp number. It does not check who sent the request.The app id alone is weak protection:
So any process or container that can reach WAHA and has seen the app id can send any message to any recipient from the paired number. It bypasses Chatwoot entirely. Putting WAHA behind a network boundary helps, but does not remove the risk from anything inside that boundary.
Describe the solution you'd like
Chatwoot already signs API-inbox webhooks. From v4.x each
Channel::Apihas asecret, and every inbox webhook delivery carries:X-Chatwoot-Timestamp: <unix seconds>X-Chatwoot-Signature: sha256=<hex HMAC-SHA256(secret, "<timestamp>.<raw body>")>(See
lib/webhooks/trigger.rb#request_headers, andapp/listeners/webhook_listener.rb, which passesinbox.channel.secret.)Proposal:
config.webhookSecretfield on the Chatwoot app (the inbox's secret from Chatwoot).Describe alternatives you've considered
hmac.key. This signs WAHA → receiver traffic only. It does not authenticate Chatwoot → WAHA.Additional context
Seen on WAHA 2026.9.1 (GOWS) with Chatwoot v4.18.0, both self-hosted.
grep -ri x-chatwoot-signature /app/distin the WAHA image returns nothing. Happy to test a build.