From 9c6b20f11e0f7b4dbdbb7fe65450a453dd8b689e Mon Sep 17 00:00:00 2001 From: Sony Mathew <2040199+sony-mathew@users.noreply.github.com> Date: Thu, 23 Apr 2026 12:23:58 +0530 Subject: [PATCH] fix(ssrf): allow provider audio downloads --- app/services/sms/incoming_message_service.rb | 1 + .../twilio/incoming_message_service.rb | 2 ++ .../incoming_message_service_helpers.rb | 1 + ...incoming_message_whatsapp_cloud_service.rb | 1 + .../sms/incoming_message_service_spec.rb | 12 ++++++++++ .../twilio/incoming_message_service_spec.rb | 14 +++++++++++ .../whatsapp/incoming_message_service_spec.rb | 21 ++++++++++++++++ ...ing_message_whatsapp_cloud_service_spec.rb | 24 +++++++++++++++++++ 8 files changed, 76 insertions(+) diff --git a/app/services/sms/incoming_message_service.rb b/app/services/sms/incoming_message_service.rb index 791699a6a..fc6ba77b8 100644 --- a/app/services/sms/incoming_message_service.rb +++ b/app/services/sms/incoming_message_service.rb @@ -101,6 +101,7 @@ class Sms::IncomingMessageService SafeFetch.fetch( media_url, http_basic_authentication: [channel.provider_config['api_key'], channel.provider_config['api_secret']], + allowed_content_type_prefixes: %w[image/ video/ audio/], allowed_content_types: Attachment::ACCEPTABLE_FILE_TYPES, & ) diff --git a/app/services/twilio/incoming_message_service.rb b/app/services/twilio/incoming_message_service.rb index 7981558b2..6ef1ccf45 100644 --- a/app/services/twilio/incoming_message_service.rb +++ b/app/services/twilio/incoming_message_service.rb @@ -175,6 +175,7 @@ class Twilio::IncomingMessageService SafeFetch.fetch( media_url, http_basic_authentication: auth_credentials, + allowed_content_type_prefixes: %w[image/ video/ audio/], allowed_content_types: Attachment::ACCEPTABLE_FILE_TYPES, & ) @@ -184,6 +185,7 @@ class Twilio::IncomingMessageService Rails.logger.info "Error downloading attachment from Twilio: #{error.message}: Retrying without auth" SafeFetch.fetch( media_url, + allowed_content_type_prefixes: %w[image/ video/ audio/], allowed_content_types: Attachment::ACCEPTABLE_FILE_TYPES, & ) diff --git a/app/services/whatsapp/incoming_message_service_helpers.rb b/app/services/whatsapp/incoming_message_service_helpers.rb index 217767221..cc28d1b31 100644 --- a/app/services/whatsapp/incoming_message_service_helpers.rb +++ b/app/services/whatsapp/incoming_message_service_helpers.rb @@ -3,6 +3,7 @@ module Whatsapp::IncomingMessageServiceHelpers SafeFetch.fetch( inbox.channel.media_url(attachment_payload[:id]), headers: inbox.channel.api_headers, + allowed_content_type_prefixes: %w[image/ video/ audio/], allowed_content_types: Attachment::ACCEPTABLE_FILE_TYPES, & ) diff --git a/app/services/whatsapp/incoming_message_whatsapp_cloud_service.rb b/app/services/whatsapp/incoming_message_whatsapp_cloud_service.rb index 7d13a284a..e93dbc26f 100644 --- a/app/services/whatsapp/incoming_message_whatsapp_cloud_service.rb +++ b/app/services/whatsapp/incoming_message_whatsapp_cloud_service.rb @@ -20,6 +20,7 @@ class Whatsapp::IncomingMessageWhatsappCloudService < Whatsapp::IncomingMessageB SafeFetch.fetch( url_response.parsed_response['url'], headers: inbox.channel.api_headers, + allowed_content_type_prefixes: %w[image/ video/ audio/], allowed_content_types: Attachment::ACCEPTABLE_FILE_TYPES, & ) diff --git a/spec/services/sms/incoming_message_service_spec.rb b/spec/services/sms/incoming_message_service_spec.rb index 3b0887663..4ad678f2d 100644 --- a/spec/services/sms/incoming_message_service_spec.rb +++ b/spec/services/sms/incoming_message_service_spec.rb @@ -100,6 +100,18 @@ describe Sms::IncomingMessageService do expect(sms_channel.inbox.messages.first.attachments.present?).to be true end + it 'creates audio attachment messages' do + stub_request(:get, 'http://test.com/test.mp3') + .to_return(status: 200, body: File.read('spec/assets/sample.mp3'), headers: { 'Content-Type' => 'audio/mpeg' }) + + media_params = { 'media': ['http://test.com/test.mp3'] }.with_indifferent_access + + described_class.new(inbox: sms_channel.inbox, params: params.merge(media_params)).perform + + attachment = sms_channel.inbox.messages.first.attachments.first + expect(attachment.file_type).to eq('audio') + end + it 'skips blocked attachment URLs' do media_params = { 'media': ['http://127.0.0.1/blocked.png'] }.with_indifferent_access diff --git a/spec/services/twilio/incoming_message_service_spec.rb b/spec/services/twilio/incoming_message_service_spec.rb index 4f2ecb4f9..6adcf88bc 100644 --- a/spec/services/twilio/incoming_message_service_spec.rb +++ b/spec/services/twilio/incoming_message_service_spec.rb @@ -179,6 +179,8 @@ describe Twilio::IncomingMessageService do before do stub_request(:get, 'https://chatwoot-assets.local/sample.png') .to_return(status: 200, body: 'image data', headers: { 'Content-Type' => 'image/png' }) + stub_request(:get, 'https://chatwoot-assets.local/sample.mp3') + .to_return(status: 200, body: File.read('spec/assets/sample.mp3'), headers: { 'Content-Type' => 'audio/mpeg' }) end let(:params_with_attachment) do @@ -207,6 +209,18 @@ describe Twilio::IncomingMessageService do expect(WebMock).to(have_requested(:get, 'https://chatwoot-assets.local/sample.png') .with { |request| request.headers['Authorization']&.start_with?('Basic ') }) end + + it 'creates an audio attachment when the provider serves audio media' do + params_with_audio_attachment = params_with_attachment.merge( + MediaContentType0: 'audio/mpeg', + MediaUrl0: 'https://chatwoot-assets.local/sample.mp3' + ) + + described_class.new(params: params_with_audio_attachment).perform + + attachment = conversation.reload.messages.last.attachments.first + expect(attachment.file_type).to eq('audio') + end end context 'when there is an error downloading the attachment' do diff --git a/spec/services/whatsapp/incoming_message_service_spec.rb b/spec/services/whatsapp/incoming_message_service_spec.rb index 76c08e984..d03bf7b2f 100644 --- a/spec/services/whatsapp/incoming_message_service_spec.rb +++ b/spec/services/whatsapp/incoming_message_service_spec.rb @@ -234,6 +234,27 @@ describe Whatsapp::IncomingMessageService do expect(whatsapp_channel.inbox.messages.first.attachments.present?).to be true end + it 'creates audio attachments when the provider serves audio media' do + stub_request(:get, whatsapp_channel.media_url('b1c68f38-8734-4ad3-b4a1-ef0c10d683')).to_return( + status: 200, + body: File.read('spec/assets/sample.mp3'), + headers: { 'Content-Type' => 'audio/mpeg' } + ) + params = { + 'contacts' => [{ 'profile' => { 'name' => 'Sojan Jose' }, 'wa_id' => '2423423243' }], + 'messages' => [{ 'from' => '2423423243', 'id' => 'SDFADSf23sfasdafasdfa', + 'audio' => { 'id' => 'b1c68f38-8734-4ad3-b4a1-ef0c10d683', + 'mime_type' => 'audio/mpeg', + 'sha256' => '29ed500fa64eb55fc19dc4124acb300e5dcca0f822a301ae99944db' }, + 'timestamp' => '1633034394', 'type' => 'audio' }] + }.with_indifferent_access + + described_class.new(inbox: whatsapp_channel.inbox, params: params).perform + + attachment = whatsapp_channel.inbox.messages.first.attachments.first + expect(attachment.file_type).to eq('audio') + end + it 'skips blocked attachment URLs' do allow(whatsapp_channel).to receive(:media_url).and_return('http://127.0.0.1/blocked.png') params = { diff --git a/spec/services/whatsapp/incoming_message_whatsapp_cloud_service_spec.rb b/spec/services/whatsapp/incoming_message_whatsapp_cloud_service_spec.rb index 4f0e17cbd..83e80687c 100644 --- a/spec/services/whatsapp/incoming_message_whatsapp_cloud_service_spec.rb +++ b/spec/services/whatsapp/incoming_message_whatsapp_cloud_service_spec.rb @@ -47,6 +47,30 @@ describe Whatsapp::IncomingMessageWhatsappCloudService do expect_message_has_attachment end + it 'creates audio attachments when the provider serves audio media' do + params[:entry][0][:changes][0][:value][:messages][0] = { + from: '2423423243', + audio: { + id: 'b1c68f38-8734-4ad3-b4a1-ef0c10d683', + mime_type: 'audio/mpeg', + sha256: '29ed500fa64eb55fc19dc4124acb300e5dcca0f822a301ae99944db' + }, + timestamp: '1664799904', + type: 'audio' + } + stub_media_url_request(url: 'https://chatwoot-assets.local/sample.mp3') + stub_request(:get, 'https://chatwoot-assets.local/sample.mp3').to_return( + status: 200, + body: File.read('spec/assets/sample.mp3'), + headers: { 'Content-Type' => 'audio/mpeg' } + ) + + described_class.new(inbox: whatsapp_channel.inbox, params: params).perform + + attachment = whatsapp_channel.inbox.messages.first.attachments.first + expect(attachment.file_type).to eq('audio') + end + it 'sends the provider headers when downloading the resolved media URL' do stub_media_url_request stub_sample_png_request