KamoCRM

Verify RingCentral webhooks before trusting them

FixVOIPService
Shipped
September 23, 2026 at 3:08 AM UTC
Author
Kamo
Commit
6f10239

RingCentral's inbound call/SMS webhooks were processed with zero verification: WebhookController routed telephony/message-store events straight into **************** **************** was `return true; // TODO`, and subscriptions were created with no verificationToken at all. The endpoint is public. Anyone who POSTed a crafted payload could inject a text into any org, forge STOP/START compliance events, trigger an inbound-call popup for an arbitrary number (toll-fraud adjacent), or spoof "from" content shown to a member — and the to-number resolution (`findByFromPhoneNumber`) was never scoped to an org, so a real customer's number could be matched and shown to the wrong tenant even by a genuine-looking payload. Fix: - RingCentralWebhookProvisioner now generates a random per-instance verificationToken (persisted in config_json, redacted from the settings API), registers subscriptions at a per-instance URL (`?instanceId=`, matching Teams/JustCall), and sends the token as **************** so RingCentral echoes it back as the `Verification-Token` header on every notification. RingCentral's own subscription URL-ownership handshake (the `Validation-Token` header WebhookController already echoes back) is untouched — that is a different header proving a different thing. - **************** now checks that header in constant time and fails closed (no token yet, no header, or a mismatch all refuse). - WebhookController resolves the instance from `?instanceId=`, requires the Verification-Token check to pass, and only then processes the event — scoped to that instance's org. **************** and **************** both gained a verifiedOrgId parameter: a to-number match in a DIFFERENT org than the verified webhook is treated as unowned, exactly like a number nobody owns, never trusted. - Deploy safety (the 3 live RingCentral instances must not lose inbound traffic): every pass of RingCentralWebhookProvisioner now reconciles the account's actual subscription list — a pre-fix subscription (no instanceId/token) is DELETED and replaced with a verified one, so live instances self-heal within ~30s of the new pod becoming leader (it already runs at startup + every 12h). For the gap before that heal completes, WebhookController still accepts an event with no instanceId at all under the pre-fix, unscoped rules — but ONLY until 2026-10-13, and every use logs a loud **************** line so the fallback is visible and not permanent. After that date it fails closed like everything else. Report for the coordinator: no action needed for the 3 live RingCentral instances — they heal automatically on next deploy. Operationally, watch for **************** in voipservice logs after this deploys; it should stop appearing within minutes. If it is still appearing close to 2026-10-13, find out why that instance's subscription was not healed (RingCentralJwtTokenService failures are logged separately) before the cutover, or extend **************** Tests: **************** (fails closed with no token/header/ wrong token, accepts the matching token, case-insensitive header), **************** (generates+persists+reuses the token; deletes a pre-fix subscription and registers a verified replacement; leaves a healthy current subscription alone), plus org-scoping cases added to VoipCallEventServiceTest and VoipMessageServiceKeywordTest. All four mutation-checked: reverting the relevant guard turns the corresponding test red, restoring it turns it green.

All changes

Like what you see shipping?

All of it arrives in your workspace on its own. Start on the free plan and read this page again in a month.

Start Free ForeverView Pricing