## Description * Added Meta webhook HMAC validation in meta_token_verify_concern.rb. * Wired it into instagram_controller.rb and whatsapp_controller.rb. * WhatsApp now verifies X-Hub-Signature-256 with WHATSAPP_APP_SECRET. * Instagram now verifies with either FB_APP_SECRET or INSTAGRAM_APP_SECRET. * Updated request specs so missing/invalid signatures return 401 and valid signatures still enqueue jobs. Fixes # (issue): [CW-6786](https://linear.app/chatwoot/issue/CW-6786/ghsa-7rw7-pc8v-mrr3-unauthenticated-message-injection-via-missing) ## Type of change Please delete options that are not relevant. - [x] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality not to work as expected) - [ ] This change requires a documentation update ## How Has This Been Tested? * Updated the controller specs and ran them successfully. * The original issue is no longer reproducible. ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [ ] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [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 - [ ] Any dependent changes have been merged and published in downstream modules --------- Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
75 lines
2.4 KiB
Ruby
75 lines
2.4 KiB
Ruby
class Webhooks::WhatsappController < ActionController::API
|
|
include MetaTokenVerifyConcern
|
|
|
|
before_action :verify_meta_signature!, only: :process_payload
|
|
|
|
def process_payload
|
|
if inactive_whatsapp_number?
|
|
Rails.logger.warn("Rejected webhook for inactive WhatsApp number: #{params[:phone_number]}")
|
|
render json: { error: 'Inactive WhatsApp number' }, status: :unprocessable_entity
|
|
return
|
|
end
|
|
|
|
Webhooks::WhatsappEventsJob.perform_later(params.to_unsafe_hash)
|
|
head :ok
|
|
end
|
|
|
|
private
|
|
|
|
def valid_token?(token)
|
|
channel = Channel::Whatsapp.find_by(phone_number: params[:phone_number])
|
|
whatsapp_webhook_verify_token = channel.provider_config['webhook_verify_token'] if channel.present?
|
|
token == whatsapp_webhook_verify_token if whatsapp_webhook_verify_token.present?
|
|
end
|
|
|
|
def meta_app_secrets
|
|
[
|
|
*channel_meta_app_secrets(whatsapp_channel),
|
|
GlobalConfigService.load('WHATSAPP_APP_SECRET', nil)
|
|
]
|
|
end
|
|
|
|
def whatsapp_channel
|
|
@whatsapp_channel ||= whatsapp_business_payload_channel || Channel::Whatsapp.find_by(phone_number: params[:phone_number])
|
|
end
|
|
|
|
def meta_signature_verification_required?
|
|
return true if whatsapp_channel.blank?
|
|
return false unless whatsapp_channel.provider == 'whatsapp_cloud'
|
|
return true if channel_meta_app_secrets(whatsapp_channel).present?
|
|
|
|
whatsapp_channel.provider_config['source'] == 'embedded_signup'
|
|
end
|
|
|
|
def whatsapp_business_payload_channel
|
|
return unless params[:object] == 'whatsapp_business_account'
|
|
|
|
metadata = params.dig(:entry, 0, :changes, 0, :value, :metadata)
|
|
return if metadata.blank?
|
|
|
|
phone_number = normalized_phone_number(metadata[:display_phone_number])
|
|
phone_number_id = metadata[:phone_number_id]
|
|
channel = Channel::Whatsapp.find_by(phone_number: phone_number)
|
|
|
|
return channel if channel && channel.provider_config['phone_number_id'] == phone_number_id
|
|
end
|
|
|
|
def normalized_phone_number(phone_number)
|
|
return if phone_number.blank?
|
|
|
|
phone_number = phone_number.to_s
|
|
phone_number.start_with?('+') ? phone_number : "+#{phone_number}"
|
|
end
|
|
|
|
def inactive_whatsapp_number?
|
|
phone_number = params[:phone_number]
|
|
return false if phone_number.blank?
|
|
|
|
inactive_numbers = GlobalConfig.get_value('INACTIVE_WHATSAPP_NUMBERS').to_s
|
|
return false if inactive_numbers.blank?
|
|
|
|
inactive_numbers_array = inactive_numbers.split(',').map(&:strip)
|
|
inactive_numbers_array.include?(phone_number)
|
|
end
|
|
end
|