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.
This commit is contained in:
parent
5a76a3669b
commit
154afa60eb
|
@ -33,6 +33,8 @@ class UploadValidator < ActiveModel::Validator
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
return true if changing_upload_security?(upload)
|
||||||
|
|
||||||
if is_authorized?(upload, extension)
|
if is_authorized?(upload, extension)
|
||||||
if FileHelper.is_supported_image?(upload.original_filename)
|
if FileHelper.is_supported_image?(upload.original_filename)
|
||||||
authorized_image_extension(upload, extension)
|
authorized_image_extension(upload, extension)
|
||||||
|
@ -44,6 +46,16 @@ class UploadValidator < ActiveModel::Validator
|
||||||
end
|
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)
|
def is_authorized?(upload, extension)
|
||||||
extension_authorized?(upload, extension, authorized_extensions(upload))
|
extension_authorized?(upload, extension, authorized_extensions(upload))
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe Jobs::UpdatePostUploadsSecureStatus do
|
||||||
|
fab!(:post) { Fabricate(:post) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
PostUpload.create!(post: post, upload: Fabricate(:upload))
|
||||||
|
PostUpload.create!(post: post, upload: Fabricate(:upload))
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when secure uploads is enabled" do
|
||||||
|
before do
|
||||||
|
setup_s3
|
||||||
|
stub_s3_store
|
||||||
|
SiteSetting.secure_media = true
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when login_required" do
|
||||||
|
before { SiteSetting.login_required = true }
|
||||||
|
|
||||||
|
it "updates all the uploads to secure" do
|
||||||
|
described_class.new.execute(post_id: post.id)
|
||||||
|
post.reload
|
||||||
|
expect(post.post_uploads.map(&:upload).map(&:secure).all?(true)).to eq(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "updates all the uploads to secure even if their extension is not authorized" do
|
||||||
|
SiteSetting.authorized_extensions = ""
|
||||||
|
described_class.new.execute(post_id: post.id)
|
||||||
|
post.reload
|
||||||
|
expect(post.post_uploads.map(&:upload).map(&:secure).all?(true)).to eq(true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -322,6 +322,15 @@ RSpec.describe UploadCreator do
|
||||||
|
|
||||||
expect(upload.secure?).to eq(false)
|
expect(upload.secure?).to eq(false)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "sets a reason for the security" do
|
||||||
|
upload = UploadCreator.new(file, filename, opts).create_for(user.id)
|
||||||
|
stored_upload = Upload.last
|
||||||
|
|
||||||
|
expect(stored_upload.secure?).to eq(true)
|
||||||
|
expect(stored_upload.security_last_changed_at).not_to eq(nil)
|
||||||
|
expect(stored_upload.security_last_changed_reason).to eq("uploading via the composer | source: upload creator")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'uploading to s3' do
|
context 'uploading to s3' do
|
||||||
|
|
|
@ -433,25 +433,25 @@ describe Upload do
|
||||||
enable_secure_media
|
enable_secure_media
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not mark an image upload as not secure when there is no access control post id, to avoid unintentional exposure' do
|
it "does not mark an image upload as not secure when there is no access control post id, to avoid unintentional exposure" do
|
||||||
upload.update!(secure: true)
|
upload.update!(secure: true)
|
||||||
upload.update_secure_status
|
upload.update_secure_status
|
||||||
expect(upload.secure).to eq(true)
|
expect(upload.secure).to eq(true)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'marks the upload as not secure if its access control post is a public post' do
|
it "marks the upload as not secure if its access control post is a public post" do
|
||||||
upload.update!(secure: true, access_control_post: Fabricate(:post))
|
upload.update!(secure: true, access_control_post: Fabricate(:post))
|
||||||
upload.update_secure_status
|
upload.update_secure_status
|
||||||
expect(upload.secure).to eq(false)
|
expect(upload.secure).to eq(false)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'leaves the upload as secure if its access control post is a PM post' do
|
it "leaves the upload as secure if its access control post is a PM post" do
|
||||||
upload.update!(secure: true, access_control_post: Fabricate(:private_message_post))
|
upload.update!(secure: true, access_control_post: Fabricate(:private_message_post))
|
||||||
upload.update_secure_status
|
upload.update_secure_status
|
||||||
expect(upload.secure).to eq(true)
|
expect(upload.secure).to eq(true)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'marks an image upload as secure if login_required is enabled' do
|
it "marks an image upload as secure if login_required is enabled" do
|
||||||
SiteSetting.login_required = true
|
SiteSetting.login_required = true
|
||||||
upload.update!(secure: false)
|
upload.update!(secure: false)
|
||||||
|
|
||||||
|
@ -461,15 +461,15 @@ describe Upload do
|
||||||
expect(upload.reload.secure).to eq(true)
|
expect(upload.reload.secure).to eq(true)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not mark an upload used for a custom emoji as secure' do
|
it "does not mark an upload used for a custom emoji as secure" do
|
||||||
SiteSetting.login_required = true
|
SiteSetting.login_required = true
|
||||||
upload.update!(secure: false)
|
upload.update!(secure: false)
|
||||||
CustomEmoji.create(name: 'meme', upload: upload)
|
CustomEmoji.create(name: "meme", upload: upload)
|
||||||
upload.update_secure_status
|
upload.update_secure_status
|
||||||
expect(upload.reload.secure).to eq(false)
|
expect(upload.reload.secure).to eq(false)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not mark an upload whose origin matches a regular emoji as secure (sometimes emojis are downloaded in pull_hotlinked_images)' do
|
it "does not mark an upload whose origin matches a regular emoji as secure (sometimes emojis are downloaded in pull_hotlinked_images)" do
|
||||||
SiteSetting.login_required = true
|
SiteSetting.login_required = true
|
||||||
falafel = Emoji.all.find { |e| e.url == "/images/emoji/twitter/falafel.png?v=#{Emoji::EMOJI_VERSION}" }
|
falafel = Emoji.all.find { |e| e.url == "/images/emoji/twitter/falafel.png?v=#{Emoji::EMOJI_VERSION}" }
|
||||||
upload.update!(secure: false, origin: "http://localhost:3000#{falafel.url}")
|
upload.update!(secure: false, origin: "http://localhost:3000#{falafel.url}")
|
||||||
|
@ -477,7 +477,7 @@ describe Upload do
|
||||||
expect(upload.reload.secure).to eq(false)
|
expect(upload.reload.secure).to eq(false)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not mark any upload with origin containing images/emoji in the URL' do
|
it "does not mark any upload with origin containing images/emoji in the URL" do
|
||||||
SiteSetting.login_required = true
|
SiteSetting.login_required = true
|
||||||
upload.update!(secure: false, origin: "http://localhost:3000/images/emoji/test.png")
|
upload.update!(secure: false, origin: "http://localhost:3000/images/emoji/test.png")
|
||||||
upload.update_secure_status
|
upload.update_secure_status
|
||||||
|
@ -491,6 +491,29 @@ describe Upload do
|
||||||
upload.update!(secure: true, access_control_post: Fabricate(:private_message_post))
|
upload.update!(secure: true, access_control_post: Fabricate(:private_message_post))
|
||||||
expect { upload.update_secure_status }.not_to raise_error
|
expect { upload.update_secure_status }.not_to raise_error
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "succeeds even if the extension of the upload is not authorized" do
|
||||||
|
upload.update!(secure: false, access_control_post: Fabricate(:private_message_post))
|
||||||
|
SiteSetting.login_required = true
|
||||||
|
SiteSetting.authorized_extensions = ""
|
||||||
|
upload.update_secure_status
|
||||||
|
upload.reload
|
||||||
|
expect(upload.secure).to eq(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "respects the authorized extensions when creating a new upload, no matter its secure status" do
|
||||||
|
SiteSetting.login_required = true
|
||||||
|
SiteSetting.authorized_extensions = ""
|
||||||
|
expect do
|
||||||
|
upl = Fabricate(
|
||||||
|
:upload,
|
||||||
|
access_control_post: Fabricate(:private_message_post),
|
||||||
|
security_last_changed_at: Time.zone.now,
|
||||||
|
security_last_changed_reason: "test",
|
||||||
|
secure: true
|
||||||
|
)
|
||||||
|
end.to raise_error(ActiveRecord::RecordInvalid)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue