17d927554dc1d4940782b51a7f520a4ca0e2b624
3 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
17d927554d |
fix: block the unused Active Storage direct-upload route (#15329)
## Description **Problem.** The default Active Storage upload route, `POST /rails/active_storage/direct_uploads`, is mounted automatically by Rails and requires no authentication. Chatwoot doesn't rely on it, our dashboard and widget uploads all use scoped, authenticated endpoints, so the route just sits there letting anyone create blobs anonymously. **Fix.** Block the built-in route so it returns `403`. Chatwoot's own upload controllers inherit from the same Rails class but are left working, an `instance_of?` check makes the block apply only to the bare route, not to the subclasses that call `super`. Fixes https://linear.app/chatwoot/issue/INF-94 ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? Added a request spec asserting the bare route returns `403` and creates no blob. Existing widget and conversation direct-upload specs still pass, confirming the scoped endpoints are unaffected. 13 examples, 0 failures across the three direct-upload specs; rubocop clean. ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes --------- Co-authored-by: Sony Mathew <sony@chatwoot.com> |
||
|
|
ffbf40c720 |
fix: harden Active Storage direct uploads and proxy streaming (#14440)
Hardens Active Storage handling on Rails 7.1 by filtering internal direct-upload metadata keys and limiting proxy range requests, while keeping audio playback on redirect URLs so large recordings are not routed through the proxy limiter. Closes - CVE-2026-33173 - CVE-2026-33174 - CVE-2026-33658 Why Rails 7.1 does not currently have patched releases for these Active Storage advisories, and Chatwoot exposes Active Storage direct-upload endpoints and media URLs. This keeps the Rails dependency unchanged while adding small local mitigations until Rails can be upgraded to 7.2.3.1+. What changed - Filters `identified`, `analyzed`, and `composed` from direct-upload blob metadata. - Limits Active Storage proxy range requests to one range under 100 MB. - Uses redirect URLs for inline audio attachments so normal playback of large recordings avoids the proxy streaming path. - Adds scoped bundle-audit ignores for the locally mitigated Active Storage advisories and the remaining Rails advisories that are not reachable through current Chatwoot usage. How to test - Upload an attachment from the dashboard reply composer and confirm it sends successfully. - Upload an attachment from the website widget and confirm it appears in the conversation. - POST a direct-upload request with `blob.metadata.identified`, `blob.metadata.analyzed`, and `blob.metadata.composed`; confirm those keys are not persisted while custom metadata remains. - Play an audio/call-recording attachment and confirm the audio URL loads through Active Storage redirect rather than proxy. - Run `bundle exec bundle audit check -v`. --------- Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com> |
||
|
|
de696a55cb |
feat(voice): add WhatsApp inbound call webhook pipeline [3] (#14315)
Adds the server-side flow that turns Meta WhatsApp Cloud Calling webhooks into Chatwoot Calls, conversations, voice_call message bubbles, and ActionCable broadcasts. Stacked on top of #14312 (PR-2 — provider methods); intentionally does not include the HTTP controller, routes, or frontend (those land in PR-4 and PR-9). ## Closes - Part of the WhatsApp Cloud Calling rollout. Linear: TBD ## What changed **Webhook routing** - `app/jobs/webhooks/whatsapp_events_job.rb` — append `prepend_mod_with('Webhooks::WhatsappEventsJob')` so EE can extend it without forking. - `enterprise/app/jobs/enterprise/webhooks/whatsapp_events_job.rb` (new) — overlay that prepends `handle_message_events` to intercept `field: 'calls'` payloads (route to `Whatsapp::IncomingCallService`) and `interactive.call_permission_reply` messages (route to `Whatsapp::CallPermissionReplyService`); falls through with `super` for regular messages. **Services** - `enterprise/app/services/whatsapp/incoming_call_service.rb` (new) — gated on `provider_config['calling_enabled']`; processes `connect` (creates inbound call via `Voice::InboundCallBuilder` or transitions an existing outbound call to `in_progress`) and `terminate` events; updates conversation `additional_attributes` and broadcasts `voice_call.incoming`/`voice_call.outbound_connected`/`voice_call.ended`. - `enterprise/app/services/whatsapp/call_permission_reply_service.rb` (new) — handles WhatsApp interactive `call_permission_reply` replies; clears the conversation's `call_permission_requested_at` flag and broadcasts `voice_call.permission_granted` so the agent UI can re-enable the call button. **Builder/model adjustments** - `enterprise/app/services/voice/inbound_call_builder.rb` — provider-agnostic; accepts `provider:` and `extra_meta:` kwargs, drops `account:` (now derived from `inbox.account` to keep the param count under rubocop's ceiling without disabling cops), uses digits-only `source_id` for WhatsApp ContactInbox (validation requires `^\d{1,15}\z`), skips Twilio-only `conference_sid` for non-Twilio providers. - `enterprise/app/services/voice/call_message_builder.rb` — adds `create!`/`update_status!` API and `CALL_TO_VOICE_STATUS` map; uses direct `Message.create!` (bypasses `Messages::MessageBuilder`'s incoming-on-non-Api-inbox guard, which would otherwise reject the system bubble); content is `'WhatsApp Call'` for WhatsApp and `'Voice Call'` for Twilio. Backwards-compatible `perform!` retained for the existing Twilio call sites. - `enterprise/app/models/call.rb` — adds `default_ice_servers` (driven by `VOICE_CALL_STUN_URLS` env), `direction_label` alias for the `inbound`/`outbound` strings the FE expects, and `ringing?`/`in_progress?`/`terminal?` predicates used throughout the pipeline. **Outgoing-channel guard** - `app/services/base/send_on_channel_service.rb` — extends `invalid_message?` to skip messages with `content_type == 'voice_call'`. Without this, agent-initiated outbound calls (PR-4) would deliver \"WhatsApp Call\" as a text message to the contact every time. **Twilio call-site update** - `enterprise/app/controllers/twilio/voice_controller.rb` — drops the now-redundant `account: current_account` kwarg from the `Voice::InboundCallBuilder.perform!` call. **Tests** - New: `spec/enterprise/services/whatsapp/incoming_call_service_spec.rb` (5 examples — calling-disabled, inbound connect, outbound connect, terminate completed, terminate no-answer, unknown event). - New: `spec/enterprise/services/whatsapp/call_permission_reply_service_spec.rb` (3 examples — accept, reject, calling-disabled). - Updated: `spec/enterprise/services/voice/inbound_call_builder_spec.rb` and `spec/enterprise/controllers/twilio/voice_controller_spec.rb` to drop the `account:` kwarg from call expectations. ## How to test In `rails console` against an account with a WhatsApp inbox where `provider_config['calling_enabled']` is true: ```ruby inbox = Inbox.find(<id>) params = { calls: [{ id: 'wacid_test', from: '15550001111', event: 'connect', session: { sdp: 'v=0...', sdp_type: 'offer' } }] } Whatsapp::IncomingCallService.new(inbox: inbox, params: params).perform # => Conversation + Call (status: 'ringing', provider: 'whatsapp') + voice_call message bubble # => ActionCable broadcasts `voice_call.incoming` to the assignee or account-wide # Then terminate it: Whatsapp::IncomingCallService.new(inbox: inbox, params: { calls: [{ id: 'wacid_test', event: 'terminate', duration: 0, terminate_reason: 'no_answer' }] } ).perform # => Call status flips to 'no_answer', message bubble updates, `voice_call.ended` broadcast fires ``` End-to-end browser flow (Meta → cable → UI) requires the controller from PR-4 and the frontend from PR-9. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |