From 5357ab332472b1ad63c51e3b8db89e302174624d Mon Sep 17 00:00:00 2001 From: Roman Rizzi Date: Wed, 9 Oct 2019 11:41:16 -0300 Subject: [PATCH] SECURITY: Safely decompress backups when restoring. (#8166) * SECURITY: Safely decompress backups when restoring. * Fix tests and update theme_controller_spec to work with zip files instead of .tar.gz --- lib/backup_restore/restorer.rb | 201 ++++++++---------- lib/compression/engine.rb | 8 +- lib/compression/pipeline.rb | 5 +- lib/compression/strategy.rb | 7 + lib/theme_store/zip_importer.rb | 4 +- .../themes/discourse-test-theme.tar.gz | Bin 3075 -> 0 bytes spec/fixtures/themes/discourse-test-theme.zip | Bin 0 -> 4734 bytes spec/lib/backup_restore/restorer_spec.rb | 81 +++++++ spec/requests/admin/themes_controller_spec.rb | 2 +- 9 files changed, 184 insertions(+), 124 deletions(-) delete mode 100644 spec/fixtures/themes/discourse-test-theme.tar.gz create mode 100644 spec/fixtures/themes/discourse-test-theme.zip diff --git a/lib/backup_restore/restorer.rb b/lib/backup_restore/restorer.rb index b67b2b56811..2480941ceb6 100644 --- a/lib/backup_restore/restorer.rb +++ b/lib/backup_restore/restorer.rb @@ -57,7 +57,7 @@ module BackupRestore ensure_directory_exists(@tmp_directory) copy_archive_to_tmp_directory - unzip_archive + decompress_archive extract_metadata validate_metadata @@ -109,6 +109,70 @@ module BackupRestore @success ? log("[SUCCESS]") : log("[FAILED]") end + ### The methods listed below are public just for testing purposes. + ### This is not a good practice, but we need to be sure that our new compression API will work. + + attr_reader :tmp_directory + + def ensure_directory_exists(directory) + log "Making sure #{directory} exists..." + FileUtils.mkdir_p(directory) + end + + def copy_archive_to_tmp_directory + if @store.remote? + log "Downloading archive to tmp directory..." + failure_message = "Failed to download archive to tmp directory." + else + log "Copying archive to tmp directory..." + failure_message = "Failed to copy archive to tmp directory." + end + + @store.download_file(@filename, @archive_filename, failure_message) + end + + def decompress_archive + return unless @is_archive + + log "Unzipping archive, this may take a while..." + + pipeline = Compression::Pipeline.new([Compression::Tar.new, Compression::Gzip.new]) + + unzipped_path = pipeline.decompress(@tmp_directory, @archive_filename) + pipeline.strip_directory(unzipped_path, @tmp_directory) + end + + def extract_metadata + metadata_path = File.join(@tmp_directory, BackupRestore::METADATA_FILE) + @metadata = if File.exists?(metadata_path) + data = Oj.load_file(@meta_filename) + raise "Failed to load metadata file." if !data + data + else + log "No metadata file to extract." + if @filename =~ /-#{BackupRestore::VERSION_PREFIX}(\d{14})/ + { "version" => Regexp.last_match[1].to_i } + else + raise "Migration version is missing from the filename." + end + end + end + + def extract_dump + @dump_filename = + if @is_archive + # For backwards compatibility + old_dump_path = File.join(@tmp_directory, BackupRestore::OLD_DUMP_FILE) + File.exists?(old_dump_path) ? old_dump_path : File.join(@tmp_directory, BackupRestore::DUMP_FILE) + else + File.join(@tmp_directory, @filename) + end + + log "Extracting dump file..." + + Compression::Gzip.new.decompress(@tmp_directory, @dump_filename) + end + protected def ensure_restore_is_enabled @@ -140,7 +204,6 @@ module BackupRestore @tmp_directory = File.join(Rails.root, "tmp", "restores", @current_db, @timestamp) @archive_filename = File.join(@tmp_directory, @filename) @tar_filename = @archive_filename[0...-3] - @meta_filename = File.join(@tmp_directory, BackupRestore::METADATA_FILE) @is_archive = !(@filename =~ /.sql.gz$/) @logs = [] @@ -194,52 +257,6 @@ module BackupRestore false end - def copy_archive_to_tmp_directory - if @store.remote? - log "Downloading archive to tmp directory..." - failure_message = "Failed to download archive to tmp directory." - else - log "Copying archive to tmp directory..." - failure_message = "Failed to copy archive to tmp directory." - end - - @store.download_file(@filename, @archive_filename, failure_message) - end - - def unzip_archive - return unless @is_archive - - log "Unzipping archive, this may take a while..." - - FileUtils.cd(@tmp_directory) do - Discourse::Utils.execute_command('gzip', '--decompress', @archive_filename, failure_message: "Failed to unzip archive.") - end - end - - def extract_metadata - @metadata = - if system('tar', '--list', '--file', @tar_filename, BackupRestore::METADATA_FILE) - log "Extracting metadata file..." - FileUtils.cd(@tmp_directory) do - Discourse::Utils.execute_command( - 'tar', '--extract', '--file', @tar_filename, BackupRestore::METADATA_FILE, - failure_message: "Failed to extract metadata file." - ) - end - - data = Oj.load_file(@meta_filename) - raise "Failed to load metadata file." if !data - data - else - log "No metadata file to extract." - if @filename =~ /-#{BackupRestore::VERSION_PREFIX}(\d{14})/ - { "version" => Regexp.last_match[1].to_i } - else - raise "Migration version is missing from the filename." - end - end - end - def validate_metadata log "Validating metadata..." log " Current version: #{@current_version}" @@ -252,31 +269,6 @@ module BackupRestore raise error if @metadata["version"] > @current_version end - def extract_dump - @dump_filename = - if @is_archive - # For backwards compatibility - if system('tar', '--list', '--file', @tar_filename, BackupRestore::OLD_DUMP_FILE) - File.join(@tmp_directory, BackupRestore::OLD_DUMP_FILE) - else - File.join(@tmp_directory, BackupRestore::DUMP_FILE) - end - else - File.join(@tmp_directory, @filename) - end - - return unless @is_archive - - log "Extracting dump file..." - - FileUtils.cd(@tmp_directory) do - Discourse::Utils.execute_command( - 'tar', '--extract', '--file', @tar_filename, File.basename(@dump_filename), - failure_message: "Failed to extract dump file." - ) - end - end - def get_dumped_by_version output = Discourse::Utils.execute_command( File.extname(@dump_filename) == '.gz' ? 'zgrep' : 'grep', @@ -293,7 +285,7 @@ module BackupRestore def restore_dump_command if File.extname(@dump_filename) == '.gz' - "gzip -d < #{@dump_filename} | #{sed_command} | #{psql_command} 2>&1" + "#{sed_command} #{@dump_filename.gsub('.gz', '')} | #{psql_command} 2>&1" else "#{psql_command} 2>&1 < #{@dump_filename}" end @@ -427,40 +419,32 @@ module BackupRestore end def extract_uploads - if system('tar', '--exclude=*/*', '--list', '--file', @tar_filename, 'uploads') - log "Extracting uploads..." + return unless File.exists?(File.join(@tmp_directory, 'uploads')) + log "Extracting uploads..." - FileUtils.cd(@tmp_directory) do - Discourse::Utils.execute_command( - 'tar', '--extract', '--keep-newer-files', '--file', @tar_filename, 'uploads/', - failure_message: "Failed to extract uploads." - ) + public_uploads_path = File.join(Rails.root, "public") + + FileUtils.cd(public_uploads_path) do + FileUtils.mkdir_p("uploads") + + tmp_uploads_path = Dir.glob(File.join(@tmp_directory, "uploads", "*")).first + previous_db_name = BackupMetadata.value_for("db_name") || File.basename(tmp_uploads_path) + current_db_name = RailsMultisite::ConnectionManagement.current_db + optimized_images_exist = File.exist?(File.join(tmp_uploads_path, 'optimized')) + + Discourse::Utils.execute_command( + 'rsync', '-avp', '--safe-links', "#{tmp_uploads_path}/", "uploads/#{current_db_name}/", + failure_message: "Failed to restore uploads." + ) + + remap_uploads(previous_db_name, current_db_name) + + if SiteSetting.Upload.enable_s3_uploads + migrate_to_s3 + remove_local_uploads(File.join(public_uploads_path, "uploads/#{current_db_name}")) end - public_uploads_path = File.join(Rails.root, "public") - - FileUtils.cd(public_uploads_path) do - FileUtils.mkdir_p("uploads") - - tmp_uploads_path = Dir.glob(File.join(@tmp_directory, "uploads", "*")).first - previous_db_name = BackupMetadata.value_for("db_name") || File.basename(tmp_uploads_path) - current_db_name = RailsMultisite::ConnectionManagement.current_db - optimized_images_exist = File.exist?(File.join(tmp_uploads_path, 'optimized')) - - Discourse::Utils.execute_command( - 'rsync', '-avp', '--safe-links', "#{tmp_uploads_path}/", "uploads/#{current_db_name}/", - failure_message: "Failed to restore uploads." - ) - - remap_uploads(previous_db_name, current_db_name) - - if SiteSetting.Upload.enable_s3_uploads - migrate_to_s3 - remove_local_uploads(File.join(public_uploads_path, "uploads/#{current_db_name}")) - end - - generate_optimized_images unless optimized_images_exist - end + generate_optimized_images unless optimized_images_exist end end @@ -661,17 +645,14 @@ module BackupRestore log "Something went wrong while marking restore as finished.", ex end - def ensure_directory_exists(directory) - log "Making sure #{directory} exists..." - FileUtils.mkdir_p(directory) - end - def after_restore_hook log "Executing the after_restore_hook..." DiscourseEvent.trigger(:restore_complete) end def log(message, ex = nil) + return if Rails.env.test? + timestamp = Time.now.strftime("%Y-%m-%d %H:%M:%S") puts(message) publish_log(message, timestamp) diff --git a/lib/compression/engine.rb b/lib/compression/engine.rb index 2cbfef4ac4b..39ee9e98a62 100644 --- a/lib/compression/engine.rb +++ b/lib/compression/engine.rb @@ -22,12 +22,6 @@ module Compression @strategy = strategy end - def decompress(dest_path, compressed_file_path, allow_non_root_folder: false) - @strategy.decompress(dest_path, compressed_file_path, allow_non_root_folder: false) - end - - def compress(path, target_name) - @strategy.compress(path, target_name) - end + delegate :extension, :decompress, :compress, :strip_directory, to: :@strategy end end diff --git a/lib/compression/pipeline.rb b/lib/compression/pipeline.rb index 7b6d3b91ea7..f955268cd5a 100644 --- a/lib/compression/pipeline.rb +++ b/lib/compression/pipeline.rb @@ -21,11 +21,10 @@ module Compression end def decompress(dest_path, compressed_file_path, allow_non_root_folder: false) - to_decompress = compressed_file_path - @strategies.reverse.each do |strategy| + @strategies.reverse.reduce(compressed_file_path) do |to_decompress, strategy| last_extension = strategy.extension strategy.decompress(dest_path, to_decompress, allow_non_root_folder: allow_non_root_folder) - to_decompress = compressed_file_path.gsub(last_extension, '') + to_decompress.gsub(last_extension, '') end end end diff --git a/lib/compression/strategy.rb b/lib/compression/strategy.rb index 8eb207a5fea..f5cd0ab1b71 100644 --- a/lib/compression/strategy.rb +++ b/lib/compression/strategy.rb @@ -32,6 +32,13 @@ module Compression end end + def strip_directory(from, to, relative: false) + sanitized_from = sanitize_path(from) + sanitized_to = sanitize_path(to) + glob_path = relative ? "#{sanitized_from}/*/*" : "#{sanitized_from}/**" + FileUtils.mv(Dir.glob(glob_path), sanitized_to) if File.directory?(sanitized_from) + end + private def sanitize_path(filename) diff --git a/lib/theme_store/zip_importer.rb b/lib/theme_store/zip_importer.rb index de0e039f9f8..c1a4aea0989 100644 --- a/lib/theme_store/zip_importer.rb +++ b/lib/theme_store/zip_importer.rb @@ -20,10 +20,8 @@ class ThemeStore::ZipImporter Dir.chdir(@temp_folder) do Compression::Engine.engine_for(@original_filename).tap do |engine| engine.decompress(@temp_folder, @filename) + engine.strip_directory(@temp_folder, @temp_folder, relative: true) end - - # --strip 1 equivalent - FileUtils.mv(Dir.glob("#{@temp_folder}/*/*"), @temp_folder) end rescue RuntimeError raise RemoteTheme::ImportError, I18n.t("themes.import_error.unpack_failed") diff --git a/spec/fixtures/themes/discourse-test-theme.tar.gz b/spec/fixtures/themes/discourse-test-theme.tar.gz deleted file mode 100644 index 1a85018ddd2ecbd40e4e3a43c67ddd03ebffa375..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3075 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 R7cT#J{s$?{)JFg=001J4DU$#I diff --git a/spec/fixtures/themes/discourse-test-theme.zip b/spec/fixtures/themes/discourse-test-theme.zip new file mode 100644 index 0000000000000000000000000000000000000000..a6aed16c9b4ffe3d8cd478b32e418ac5ac097325 GIT binary patch literal 4734 zcmbuCc|6p67stnv-DF>qESD_Pn9$fFOR~(!zC_j-yO}a$%cvOQ3JJ*;vS(~rifT~y zy$RRS$euz7gVOS-d!Lf0uI_Vxzt?a3=X1{IoZt8RI&VWb6*U9k^P!O`qyOdQuNyyr z1>od{c0~DjqY+?Ngo6{p8|>zYLZZPKSA-V=Vs?rN03N()GjF9~O{U-g82(gBbT=ta zl%sKk8r{L`d zw@nN5VS3&AkakEHoOrUkrwvkWY^YAxgbOR;e7d{>!(5TY8wZ!4^#y-%dtnW@2-$L~ z{+HVy=O(sWZVqQqJ{T!?Gzz&hr>0mH;N!zgVe9F_?NJ-Nu`D3UM~#5M>v?&_u4&@B zw5H^RMx+D@5YV==zC54P6B=8<)JlkTF%tAV4}DudPr7n~7jvFIK)p=L@- z>OMT4A`o|UlxN!R$XI3GME?c;u%|uk&wy|luNYH16Ka7}3Hy7waZ#6vLW@V%QSc`M zhz7pTKY5p*uvzU^c^*;xtDoEfjYeSp$+A7PYtjEy^hCL!q|PE;c2@rS^|z|MSE*V? z7j#}ejdIWy@-`{)RFoSKXVg7p0grh+)k<^)%^#m`B`Bh!-J@A-vQnbjhjL_GWCi@< z->rUNuf5tbM?QbdR<-Ve|A==(mS5W?1OPBNed&%0`3C-C(3$YT(bd&aOL~B?le|mX z$S8w_$)`H%zWWqR6b8I+O83t97|`f*Diua0sWQwwdkpT&gxi+wf&1^HzS{+|gjWR? zQCeJ_qUvkXfL;ZV1D?Kmt!A?k#958i1U?z8=#h8m2}^Cw54FSbS{|2ou428zGU%Q* z?W{6>FLRcb%~md5I-hp0S=hMQEy0mvOt+OA@FJ_(tc;cST zirP506wd@Q9GxWh^>N=E50a#VQmruGA7uls#Rr_2MM9par4rTXNcI3+zQ59bSO1*) zr9nqEBrvyr3%)@F`=r?cJced1@06kk$F>-XLtHtSaL+aijI!k*LYhoh$nG8}W=KYIl z87T&UD;qCdN+oQSsnTaQTF$pR&)LoXn!mQdi6z6G0c2 zalLqERmL%i$Gaucj{#7&~Oxwt?43sgCSo>Rk{ z!jhr$v>C+bC%i<4E*?RAOo}g%pPrX}DcS5~rzYRBbWd&soj#hlX0FBO7Dg_UuT_cf z>pigP$mOQtoTlpGnil~xmw6d}sq(U1(Hzg3OZvcqle?gWZ>h3sv*ytf!j;2EE_>ug zyq~8tz#d<^ZE|>Dmcg*#3N~%mpNce{E7Igop5(QsFRjhV3;h*25VtYgSY} z?vB8|%I8WI0#1kjvEgNyH8=T&YjY#O8hxmGQ}QAVEocs*hRd=qM=OUlUvm~?H;Re5d!wQEZZOqjUowA#1qokOx5yW0oln4-RoX;! zkA&Bho?B3CWT6Wcp=ZaiM%pm8;0dSBtr4_Wn-qXo>R!po;TX0Zm z>4+Y?yPUvvX7LSmc&oI`2Si@rWJ38(fkqfhc^hk|=$>rv!0yDg-|37pQ%%6KLyTP$ z(``YuIwIoF2UVPXNhG5-QevapyZ!}#clC4yy+^|jFBVA1bxDcaEKxofOh`J>-aLD` z>UN}~o-@v_c?^{mW7rg>me+J@(~p1FhIul(h$z} z&GRi5)Sf!sC+F8@G~Ri`HSZlHxivf&(C>XWr*MF#xkSP%Ig#&}TV@Pr0PB^vcPi0r zjsdHD0bORE&)URdJz@&s;JNCA$<3w=ls22zv4jiV(0%muu)amKw2%&q&0v! z$t&a8A{cAB;qo4p@@Pu_fvEafHL_)fq_xZH?N%;5c9s|ZOh>wDnfaqqL5gO*RYIMp z>RSt<7CtlVaqJS-@(WV1NE1f0_j<`=;`8odoGBp~)D^4f;60N~k}^5E0){oF)|{HU zlS#t?RGi32d1_DbWofVU$)1_fs5HB zXs_cEhNy6ck$S?tjZVs-g-*`A2i1%(LP86>8y~FQElzy6M?AHwNYZDz(A%?WO(Nu! zhG`w_m4*Rrl5+Lry9;f@D-5>vEfvK56HZxooL)Q&hoQ8TrhM(V$z1{O9ti^!(j*IP zFL(pX%4;>WRnI;Y%fu1_59bcY^hzHvqz>g}g-m8=Eyjilop{CFq!BA!hF7On_Pr6J zTw{f8aPgt>~(^f(CKU{8|q#%~fh6gO%% z`DtO7i_WumGIc*!kea$e@*~EBSU)uL->@wa$g4v*YDNW0<+7qjvy&mi23p42q6(>p zz!x-)#k^+KhwKl&VMxG6*ARwpKAOokdP(;w+s51p_OwTIB**Vl=Q5@C&!L!CD0^e z<$~airTLV`vSUo@qOU=Ll6qn%=cod z$ar(j+n_n>i3eQw7xth2Pb-(xj4O`GBH_O@&Jt6mjeJ z!Xlay=zHpPGrjUF)z5RAf6BpU@96-5w_Dd|IQj8tE3w;@wKcV2+>kElAE6PbyXPcW zA1u+<3BPA7z6BE$UxsQs23~=02th%~N%b>qWZIf5zZ`0>KTqJhK_uN*h%|)%TvimY zT^7EZ>^Gp?DgUiv+d&VsO+F_${a5Ax3zu|X;qvc1h-?e;TK&M*=>4~R{gohpZecsu zNpDln2|oLa>N{xi|1q+>Td1$t_;VxMkqz>l5lzZn%Jd_I+-c)m)3<|<*mr@(b_=wf z2X_Ye@ml!4;Z)SSIQdFbKd;mF^?&HQ^v!-z{To^B^zf}n+t<3pcTpZN@8aS6on?E7 UJv5&;1IDdWb}LmV@blMy0C`J1Y5)KL literal 0 HcmV?d00001 diff --git a/spec/lib/backup_restore/restorer_spec.rb b/spec/lib/backup_restore/restorer_spec.rb index 49f829b27e5..2968156d94a 100644 --- a/spec/lib/backup_restore/restorer_spec.rb +++ b/spec/lib/backup_restore/restorer_spec.rb @@ -19,4 +19,85 @@ describe BackupRestore::Restorer do expect(described_class.pg_produces_portable_dump?(key)).to eq(value) end end + + describe 'Decompressing a backup' do + fab!(:admin) { Fabricate(:admin) } + + before do + SiteSetting.allow_restore = true + @restore_path = File.join(Rails.root, "public", "backups", RailsMultisite::ConnectionManagement.current_db) + end + + after do + FileUtils.rm_rf @restore_path + FileUtils.rm_rf @restorer.tmp_directory + end + + context 'When there are uploads' do + before do + @restore_folder = "backup-#{SecureRandom.hex}" + @temp_folder = "#{@restore_path}/#{@restore_folder}" + FileUtils.mkdir_p("#{@temp_folder}/uploads") + + Dir.chdir(@restore_path) do + File.write("#{@restore_folder}/dump.sql", 'This is a dump') + Compression::Gzip.new.compress(@restore_folder, 'dump.sql') + FileUtils.rm_rf("#{@restore_folder}/dump.sql") + File.write("#{@restore_folder}/uploads/upload.txt", 'This is an upload') + + Compression::Tar.new.compress(@restore_path, @restore_folder) + end + + Compression::Gzip.new.compress(@restore_path, "#{@restore_folder}.tar") + FileUtils.rm_rf @temp_folder + + build_restorer("#{@restore_folder}.tar.gz") + end + + it '#decompress_archive works correctly' do + @restorer.decompress_archive + + expect(exists?("dump.sql.gz")).to eq(true) + expect(exists?("uploads", directory: true)).to eq(true) + end + + it '#extract_dump works correctly' do + @restorer.decompress_archive + @restorer.extract_dump + + expect(exists?('dump.sql')).to eq(true) + end + end + + context 'When restoring a single file' do + before do + FileUtils.mkdir_p(@restore_path) + + Dir.chdir(@restore_path) do + File.write('dump.sql', 'This is a dump') + Compression::Gzip.new.compress(@restore_path, 'dump.sql') + FileUtils.rm_rf('dump.sql') + end + + build_restorer('dump.sql.gz') + end + + it '#extract_dump works correctly with a single file' do + @restorer.extract_dump + + expect(exists?("dump.sql")).to eq(true) + end + end + + def exists?(relative_path, directory: false) + full_path = "#{@restorer.tmp_directory}/#{relative_path}" + directory ? File.directory?(full_path) : File.exists?(full_path) + end + + def build_restorer(filename) + @restorer = described_class.new(admin.id, filename: filename) + @restorer.ensure_directory_exists(@restorer.tmp_directory) + @restorer.copy_archive_to_tmp_directory + end + end end diff --git a/spec/requests/admin/themes_controller_spec.rb b/spec/requests/admin/themes_controller_spec.rb index 9b31a60d690..8e6f83a276d 100644 --- a/spec/requests/admin/themes_controller_spec.rb +++ b/spec/requests/admin/themes_controller_spec.rb @@ -74,7 +74,7 @@ describe Admin::ThemesController do end let(:theme_archive) do - Rack::Test::UploadedFile.new(file_from_fixtures("discourse-test-theme.tar.gz", "themes"), "application/x-gzip") + Rack::Test::UploadedFile.new(file_from_fixtures("discourse-test-theme.zip", "themes"), "application/zip") end let(:image) do