discourse/lib/validators/upload_validator.rb
Martin Brennan 154afa60eb
FIX: Skip upload extension validation when changing security (#16498)
When changing upload security using `Upload#update_secure_status`,
we may not have the context of how an upload is being created, because
this code path can be run through scheduled jobs. When calling
update_secure_status, the normal ActiveRecord validations are run,
and ours include validating extensions. In some cases the upload
is created in an automated way, such as user export zips, and the
security is applied later, with the extension prohibited from
use when normally uploading.

This caused the upload to fail validation on `update_secure_status`,
causing the security change to silently fail. This fixes the issue
by skipping the file extension validation when the upload security
is being changed.
2022-04-20 14:11:39 +10:00

162 lines
4.5 KiB
Ruby

# frozen_string_literal: true
require_dependency "file_helper"
module Validators; end
class UploadValidator < ActiveModel::Validator
def validate(upload)
# staff can upload any file in PM
if (upload.for_private_message && SiteSetting.allow_staff_to_upload_any_file_in_pm)
return true if upload.user&.staff?
end
# check the attachment blocklist
if upload.for_group_message && SiteSetting.allow_all_attachments_for_group_messages
return upload.original_filename =~ SiteSetting.blocked_attachment_filenames_regex
end
extension = File.extname(upload.original_filename)[1..-1] || ""
if upload.for_site_setting &&
upload.user&.staff? &&
FileHelper.is_supported_image?(upload.original_filename)
return true
end
if upload.for_gravatar &&
FileHelper.supported_gravatar_extensions.include?(extension)
maximum_image_file_size(upload)
return true
end
return true if changing_upload_security?(upload)
if is_authorized?(upload, extension)
if FileHelper.is_supported_image?(upload.original_filename)
authorized_image_extension(upload, extension)
maximum_image_file_size(upload)
else
authorized_attachment_extension(upload, extension)
maximum_attachment_file_size(upload)
end
end
end
# this should only be run on existing records, and covers cases of
# upload.update_secure_status being run outside of the creation flow,
# where some cases e.g. have exemptions on the extension enforcement
def changing_upload_security?(upload)
!upload.new_record? && \
upload.changed_attributes.keys.all? do |attribute|
%w(secure security_last_changed_at security_last_changed_reason).include?(attribute)
end
end
def is_authorized?(upload, extension)
extension_authorized?(upload, extension, authorized_extensions(upload))
end
def authorized_image_extension(upload, extension)
extension_authorized?(upload, extension, authorized_images(upload))
end
def maximum_image_file_size(upload)
maximum_file_size(upload, "image")
end
def authorized_attachment_extension(upload, extension)
extension_authorized?(upload, extension, authorized_attachments(upload))
end
def maximum_attachment_file_size(upload)
maximum_file_size(upload, "attachment")
end
private
def extensions_to_set(exts)
extensions = Set.new
exts
.gsub(/[\s\.]+/, "")
.downcase
.split("|")
.each { |extension| extensions << extension unless extension.include?("*") }
extensions
end
def authorized_extensions(upload)
extensions = if upload.for_theme
SiteSetting.theme_authorized_extensions
elsif upload.for_export
SiteSetting.export_authorized_extensions
else
SiteSetting.authorized_extensions
end
extensions_to_set(extensions)
end
def authorized_images(upload)
authorized_extensions(upload) & FileHelper.supported_images
end
def authorized_attachments(upload)
authorized_extensions(upload) - FileHelper.supported_images
end
def authorizes_all_extensions?(upload)
if upload.user&.staff?
return true if SiteSetting.authorized_extensions_for_staff.include?("*")
end
extensions = if upload.for_theme
SiteSetting.theme_authorized_extensions
elsif upload.for_export
SiteSetting.export_authorized_extensions
else
SiteSetting.authorized_extensions
end
extensions.include?("*")
end
def extension_authorized?(upload, extension, extensions)
return true if authorizes_all_extensions?(upload)
staff_extensions = Set.new
if upload.user&.staff?
staff_extensions = extensions_to_set(SiteSetting.authorized_extensions_for_staff)
return true if staff_extensions.include?(extension.downcase)
end
unless authorized = extensions.include?(extension.downcase)
message = I18n.t("upload.unauthorized", authorized_extensions: (extensions | staff_extensions).to_a.join(", "))
upload.errors.add(:original_filename, message)
end
authorized
end
def maximum_file_size(upload, type)
max_size_kb = if upload.for_export
SiteSetting.max_export_file_size_kb
else
SiteSetting.get("max_#{type}_size_kb")
end
max_size_bytes = max_size_kb.kilobytes
if upload.filesize > max_size_bytes
message = I18n.t(
"upload.#{type}s.too_large_humanized",
max_size: ActiveSupport::NumberHelper.number_to_human_size(max_size_bytes)
)
upload.errors.add(:filesize, message)
end
end
end