mirror of
https://github.com/discourse/discourse.git
synced 2024-11-23 15:05:24 +08:00
59867cc091
### Background When SSRF detection fails, the exception bubbles all the way up, causing a log alert. This isn't actionable, and should instead be ignored. The existing `rescue` does already ignore network errors, but fails to account for SSRF exceptions coming from `FinalDestination`. ### What is this change? This PR does two things. --- Firstly, it introduces a common root exception class, `FinalDestination::SSRFError` for SSRF errors. This serves two functions: 1) it makes it easier to rescue both errors at once, which is generally what one wants to do and 2) prevents having to dig deep into the class hierarchy for the constant. This change is fully backwards compatible thanks to how inheritance and exception handling works. --- Secondly, it rescues this new exception in `UserAvatar.import_url_for_user`, which is causing sporadic errors to be logged in production. After this SSRF errors are handled the same as network errors.
198 lines
6.1 KiB
Ruby
198 lines
6.1 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
class UserAvatar < ActiveRecord::Base
|
|
belongs_to :user
|
|
belongs_to :gravatar_upload, class_name: "Upload"
|
|
belongs_to :custom_upload, class_name: "Upload"
|
|
has_many :upload_references, as: :target, dependent: :destroy
|
|
|
|
after_save do
|
|
if saved_change_to_custom_upload_id? || saved_change_to_gravatar_upload_id?
|
|
upload_ids = [self.custom_upload_id, self.gravatar_upload_id]
|
|
UploadReference.ensure_exist!(upload_ids: upload_ids, target: self)
|
|
end
|
|
end
|
|
|
|
@@custom_user_gravatar_email_hash = {
|
|
Discourse::SYSTEM_USER_ID => User.email_hash("info@discourse.org"),
|
|
}
|
|
|
|
def self.register_custom_user_gravatar_email_hash(user_id, email)
|
|
@@custom_user_gravatar_email_hash[user_id] = User.email_hash(email)
|
|
end
|
|
|
|
def contains_upload?(id)
|
|
gravatar_upload_id == id || custom_upload_id == id
|
|
end
|
|
|
|
def update_gravatar!
|
|
DistributedMutex.synchronize("update_gravatar_#{user_id}") do
|
|
begin
|
|
self.update!(last_gravatar_download_attempt: Time.zone.now)
|
|
|
|
max = Discourse.avatar_sizes.max
|
|
|
|
# The user could be deleted before this executes
|
|
return if user.blank? || user.primary_email.blank?
|
|
|
|
email_hash = @@custom_user_gravatar_email_hash[user_id] || user.email_hash
|
|
gravatar_url =
|
|
"https://#{SiteSetting.gravatar_base_url}/avatar/#{email_hash}.png?s=#{max}&d=404&reset_cache=#{SecureRandom.urlsafe_base64(5)}"
|
|
|
|
if SiteSetting.verbose_upload_logging
|
|
Rails.logger.warn("Verbose Upload Logging: Downloading gravatar from #{gravatar_url}")
|
|
end
|
|
|
|
# follow redirects in case gravatar change rules on us
|
|
tempfile =
|
|
FileHelper.download(
|
|
gravatar_url,
|
|
max_file_size: SiteSetting.max_image_size_kb.kilobytes,
|
|
tmp_file_name: "gravatar",
|
|
skip_rate_limit: true,
|
|
verbose: false,
|
|
follow_redirect: true,
|
|
)
|
|
|
|
if tempfile
|
|
ext = File.extname(tempfile)
|
|
ext = ".png" if ext.blank?
|
|
|
|
upload =
|
|
UploadCreator.new(
|
|
tempfile,
|
|
"gravatar#{ext}",
|
|
origin: gravatar_url,
|
|
type: "avatar",
|
|
for_gravatar: true,
|
|
).create_for(user_id)
|
|
|
|
if gravatar_upload_id != upload.id
|
|
User.transaction do
|
|
if gravatar_upload_id && user.uploaded_avatar_id == gravatar_upload_id
|
|
user.update!(uploaded_avatar_id: upload.id)
|
|
end
|
|
|
|
self.update!(gravatar_upload: upload)
|
|
end
|
|
end
|
|
end
|
|
rescue OpenURI::HTTPError => e
|
|
raise e if e.io&.status[0].to_i != 404
|
|
ensure
|
|
tempfile&.close!
|
|
end
|
|
end
|
|
end
|
|
|
|
def self.local_avatar_url(hostname, username, upload_id, size)
|
|
self.local_avatar_template(hostname, username, upload_id).gsub("{size}", size.to_s)
|
|
end
|
|
|
|
def self.local_avatar_template(hostname, username, upload_id)
|
|
version = self.version(upload_id)
|
|
"#{Discourse.base_path}/user_avatar/#{hostname}/#{username}/{size}/#{version}.png"
|
|
end
|
|
|
|
def self.external_avatar_url(user_id, upload_id, size)
|
|
self.external_avatar_template(user_id, upload_id).gsub("{size}", size.to_s)
|
|
end
|
|
|
|
def self.external_avatar_template(user_id, upload_id)
|
|
version = self.version(upload_id)
|
|
"#{Discourse.store.absolute_base_url}/avatars/#{user_id}/{size}/#{version}.png"
|
|
end
|
|
|
|
def self.version(upload_id)
|
|
"#{upload_id}_#{OptimizedImage::VERSION}"
|
|
end
|
|
|
|
def self.import_url_for_user(avatar_url, user, options = nil)
|
|
if SiteSetting.verbose_upload_logging
|
|
Rails.logger.warn("Verbose Upload Logging: Downloading sso-avatar from #{avatar_url}")
|
|
end
|
|
|
|
tempfile =
|
|
FileHelper.download(
|
|
avatar_url,
|
|
max_file_size: SiteSetting.max_image_size_kb.kilobytes,
|
|
tmp_file_name: "sso-avatar",
|
|
follow_redirect: true,
|
|
)
|
|
|
|
return unless tempfile
|
|
|
|
ext = FastImage.type(tempfile).to_s
|
|
tempfile.rewind
|
|
|
|
upload =
|
|
UploadCreator.new(
|
|
tempfile,
|
|
"external-avatar." + ext,
|
|
origin: avatar_url,
|
|
type: "avatar",
|
|
).create_for(user.id)
|
|
|
|
user.create_user_avatar! unless user.user_avatar
|
|
|
|
if !user.user_avatar.contains_upload?(upload.id)
|
|
user.user_avatar.update!(custom_upload_id: upload.id)
|
|
override_gravatar = !options || options[:override_gravatar]
|
|
|
|
if user.uploaded_avatar_id.nil? ||
|
|
!user.user_avatar.contains_upload?(user.uploaded_avatar_id) || override_gravatar
|
|
user.update!(uploaded_avatar_id: upload.id)
|
|
end
|
|
end
|
|
rescue Net::ReadTimeout, OpenURI::HTTPError, FinalDestination::SSRFError
|
|
# Skip saving. We are not connected to the net, or SSRF checks failed.
|
|
ensure
|
|
tempfile.close! if tempfile && tempfile.respond_to?(:close!)
|
|
end
|
|
|
|
def self.ensure_consistency!
|
|
DB.exec <<~SQL
|
|
UPDATE user_avatars
|
|
SET gravatar_upload_id = NULL
|
|
WHERE gravatar_upload_id IN (
|
|
SELECT u1.gravatar_upload_id FROM user_avatars u1
|
|
LEFT JOIN uploads up
|
|
ON u1.gravatar_upload_id = up.id
|
|
WHERE u1.gravatar_upload_id IS NOT NULL AND
|
|
up.id IS NULL
|
|
)
|
|
SQL
|
|
|
|
DB.exec <<~SQL
|
|
UPDATE user_avatars
|
|
SET custom_upload_id = NULL
|
|
WHERE custom_upload_id IN (
|
|
SELECT u1.custom_upload_id FROM user_avatars u1
|
|
LEFT JOIN uploads up
|
|
ON u1.custom_upload_id = up.id
|
|
WHERE u1.custom_upload_id IS NOT NULL AND
|
|
up.id IS NULL
|
|
)
|
|
SQL
|
|
end
|
|
end
|
|
|
|
# == Schema Information
|
|
#
|
|
# Table name: user_avatars
|
|
#
|
|
# id :integer not null, primary key
|
|
# user_id :integer not null
|
|
# custom_upload_id :integer
|
|
# gravatar_upload_id :integer
|
|
# last_gravatar_download_attempt :datetime
|
|
# created_at :datetime not null
|
|
# updated_at :datetime not null
|
|
#
|
|
# Indexes
|
|
#
|
|
# index_user_avatars_on_custom_upload_id (custom_upload_id)
|
|
# index_user_avatars_on_gravatar_upload_id (gravatar_upload_id)
|
|
# index_user_avatars_on_user_id (user_id)
|
|
#
|