refactor(ssrf): centralize safe fetch allowlist

This commit is contained in:
Sony Mathew
2026-04-23 12:31:51 +05:30
parent 9c6b20f11e
commit 1d4c2adef7
8 changed files with 60 additions and 9 deletions
@@ -28,7 +28,6 @@ class Messages::Messenger::MessageBuilder
def attach_file(attachment, file_url)
SafeFetch.fetch(
file_url,
allowed_content_type_prefixes: %w[image/ video/ audio/],
allowed_content_types: Attachment::ACCEPTABLE_FILE_TYPES
) do |attachment_file|
attachment.file.attach(
@@ -101,7 +101,6 @@ 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,
&
)
@@ -150,7 +150,6 @@ class Telegram::IncomingMessageService
SafeFetch.fetch(
file_download_path,
allowed_content_type_prefixes: %w[image/ video/ audio/],
allowed_content_types: Attachment::ACCEPTABLE_FILE_TYPES
) do |attachment_file|
build_file_attachment(attachment_file)
@@ -175,7 +175,6 @@ 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,
&
)
@@ -185,7 +184,6 @@ 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,
&
)
@@ -3,7 +3,6 @@ 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,
&
)
@@ -20,7 +20,6 @@ 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,
&
)
+20 -2
View File
@@ -2,8 +2,26 @@ require 'ssrf_filter'
# rubocop:disable Metrics/ModuleLength
module SafeFetch
DEFAULT_ALLOWED_CONTENT_TYPE_PREFIXES = %w[image/ video/].freeze
DEFAULT_ALLOWED_CONTENT_TYPES = [].freeze
DEFAULT_ALLOWED_CONTENT_TYPE_PREFIXES = %w[image/ video/ audio/].freeze
DEFAULT_ALLOWED_CONTENT_TYPES = %w[
text/csv
text/plain
text/rtf
application/json
application/pdf
application/zip
application/x-7z-compressed
application/vnd.rar
application/x-tar
application/msword
application/vnd.ms-excel
application/vnd.ms-powerpoint
application/rtf
application/vnd.oasis.opendocument.text
application/vnd.openxmlformats-officedocument.presentationml.presentation
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
application/vnd.openxmlformats-officedocument.wordprocessingml.document
].freeze
DEFAULT_SENSITIVE_HEADERS = %w[authorization cookie].freeze
DEFAULT_OPEN_TIMEOUT = 2
DEFAULT_READ_TIMEOUT = 20
+40
View File
@@ -195,6 +195,46 @@ RSpec.describe SafeFetch do
expect { described_class.fetch(url) { nil } }.not_to raise_error
end
it 'allows audio/mpeg responses' do
stub_request(:get, url).to_return(
status: 200,
body: File.new(Rails.root.join('spec/assets/sample.mp3')),
headers: { 'Content-Type' => 'audio/mpeg' }
)
expect { described_class.fetch(url) { nil } }.not_to raise_error
end
it 'allows application/pdf responses' do
stub_request(:get, url).to_return(
status: 200,
body: File.new(Rails.root.join('spec/assets/sample.pdf')),
headers: { 'Content-Type' => 'application/pdf' }
)
expect { described_class.fetch(url) { nil } }.not_to raise_error
end
it 'allows text/plain responses' do
stub_request(:get, url).to_return(
status: 200,
body: 'plain-text',
headers: { 'Content-Type' => 'text/plain' }
)
expect { described_class.fetch(url) { nil } }.not_to raise_error
end
it 'allows text/csv responses' do
stub_request(:get, url).to_return(
status: 200,
body: "id,name\n1,Chatwoot\n",
headers: { 'Content-Type' => 'text/csv' }
)
expect { described_class.fetch(url) { nil } }.not_to raise_error
end
it 'strips charset/boundary parameters before comparing' do
stub_request(:get, url).to_return(
status: 200,