- 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.
