CVE-2021-21288: Server-Side Request Forgery (SSRF) (CWE-918)
value: CarrierWave is an open-source RubyGem which provides a simple and flexible way to upload files from Ruby applications. In CarrierWave before versions 1.3.2 and 2.1.1 the download feature has an SSRF vulnerability, allowing attacks to provide DNS entries or IP addresses that are intended for internal use and gather information about the Intranet infrastructure of the platform. This is fixed in versions 1.3.2 and 2.1.1.
CWE description: The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination.
CVSS v3 base score: 4.3
Published: 2021-02-08T20:15Z
Repository: https://github.com/carrierwaveuploader/carrierwave
Language: Ruby
Files changed: carrierwave.gemspec, features/step_definitions/download_steps.rb, lib/carrierwave/downloader/base.rb, lib/carrierwave/downloader/remote_file.rb, spec/downloader/base_spec.rb, spec/downloader/remote_file_spec.rb, spec/mount_multiple_spec.rb, spec/mount_single_spec.rb, spec/orm/activerecord_spec.rb, spec/spec_helper.rb, spec/uploader/download_spec.rb
Commit date: 2021-02-08 13:42:22 +0900
Commit message: Fix SSRF vulnerability in the remote file download feature

Closes #2509, Refs. https://github.com/carrierwaveuploader/carrierwave/security/advisories/GHSA-fwcm-636p-68r5

Diff: diff --git a/carrierwave.gemspec b/carrierwave.gemspec
index 4bcfec2..9ff5931 100644
--- a/carrierwave.gemspec
+++ b/carrierwave.gemspec
@@ -25,10 +25,11 @@ Gem::Specification.new do |s|
   s.add_dependency "activemodel", ">= 5.0.0"
   s.add_dependency "mini_mime", ">= 0.1.3"
   s.add_dependency "image_processing", "~> 1.1"
   s.add_dependency "mimemagic", ">= 0.3.0"
   s.add_dependency "addressable", "~> 2.6"
+  s.add_dependency "ssrf_filter", "~> 1.0"
   if RUBY_ENGINE == 'jruby'
     s.add_development_dependency 'activerecord-jdbcpostgresql-adapter'
   else
     s.add_development_dependency "pg"
   end
diff --git a/features/step_definitions/download_steps.rb b/features/step_definitions/download_steps.rb
index b36acfa..63a9295 100644
--- a/features/step_definitions/download_steps.rb
+++ b/features/step_definitions/download_steps.rb
@@ -1,8 +1,8 @@
 When /^I download the file '([^']+)'/ do |url|
   unless ENV['REMOTE'] == 'true'
-    stub_request(:get, "s3.amazonaws.com/Monkey/testfile.txt").
+    stub_request(:get, %r{/Monkey/testfile.txt}).
       to_return(body: "S3 Remote File", headers: { "Content-Type" => "text/plain" })
   end
 
   @uploader.download!(url)
 end
diff --git a/lib/carrierwave/downloader/base.rb b/lib/carrierwave/downloader/base.rb
index 12a39f2..b040bbc 100644
--- a/lib/carrierwave/downloader/base.rb
+++ b/lib/carrierwave/downloader/base.rb
@@ -1,6 +1,7 @@
 require 'open-uri'
+require 'ssrf_filter'
 require 'addressable'
 require 'carrierwave/downloader/remote_file'
 
 module CarrierWave
   module Downloader
@@ -20,16 +21,26 @@ module CarrierWave
       # [remote_headers (Hash)] Request headers
       #
       def download(url, remote_headers = {})
         headers = remote_headers.
           reverse_merge('User-Agent' => "CarrierWave/#{CarrierWave::VERSION}")
+        uri = process_uri(url.to_s)
         begin
-          file = OpenURI.open_uri(process_uri(url.to_s), headers)
+          if skip_ssrf_protection?(uri)
+            response = OpenURI.open_uri(process_uri(url.to_s), headers)
+          else
+            request = nil
+            response = SsrfFilter.get(uri, headers: headers) do |req|
+              request = req
+            end
+            response.uri = request.uri
+            response.value
+          end
         rescue StandardError => e
           raise CarrierWave::DownloadError, "could not download file: #{e.message}"
         end
-        CarrierWave::Downloader::RemoteFile.new(file)
+        CarrierWave::Downloader::RemoteFile.new(response)
       end
 
       ##
       # Processes the given URL by parsing it, and escaping if necessary. Public to allow overriding.
       #
@@ -47,8 +58,30 @@ module CarrierWave
           URI.parse("#{encoded_uri}#{URI::DEFAULT_PARSER.escape(query)}")
         end
       rescue URI::InvalidURIError, Addressable::URI::InvalidURIError
         raise CarrierWave::DownloadError, "couldn't parse URL: #{uri}"
       end
+
+      ##
+      # If this returns true, SSRF protection will be bypassed.
+      # You can override this if you want to allow accessing specific local URIs that are not SSRF exploitable.
+      #
+      # === Parameters
+      #
+      # [uri (URI)] The URI where the remote file is stored
+      #
+      # === Examples
+      #
+      #     class CarrierWave::Downloader::CustomDownloader < CarrierWave::Downloader::Base
+      #       def skip_ssrf_protection?(uri)
+      #         uri.hostname == 'localhost' && uri.port == 80
+      #       end
+      #     end
+      #
+      #     my_uploader.downloader = CarrierWave::Downloader::CustomDownloader
+      #
+      def skip_ssrf_protection?(uri)
+        false
+      end
     end
   end
 end
diff --git a/lib/carrierwave/downloader/remote_file.rb b/lib/carrierwave/downloader/remote_file.rb
index 654e5a2..d90a34e 100644
--- a/lib/carrierwave/downloader/remote_file.rb
+++ b/lib/carrierwave/downloader/remote_file.rb
@@ -1,17 +1,38 @@
 module CarrierWave
   module Downloader
     class RemoteFile
-      attr_reader :file
+      attr_reader :file, :uri
 
       def initialize(file)
-        @file = file.is_a?(String) ? StringIO.new(file) : file
+        case file
+        when String
+          @file = StringIO.new(file)
+        when Net::HTTPResponse
+          @file = StringIO.new(file.body)
+          @content_type = file.content_type
+          @headers = file
+          @uri = file.uri
+        else
+          @file = file
+          @content_type = file.content_type
+          @headers = file.meta
+          @uri = file.base_uri
+        end
+      end
+
+      def content_type
+        @content_type || 'application/octet-stream'
+      end
+
+      def headers
+        @headers || {}
       end
 
       def original_filename
         filename = filename_from_header || filename_from_uri
-        mime_type = MiniMime.lookup_by_content_type(file.content_type)
+        mime_type = MiniMime.lookup_by_content_type(content_type)
         unless File.extname(filename).present? || mime_type.blank?
           filename = "#{filename}.#{mime_type.extension}"
         end
         filename
       end
@@ -21,20 +42,20 @@ module CarrierWave
       end
 
       private
 
       def filename_from_header
-        return nil unless file.meta.include? 'content-disposition'
+        return nil unless headers['content-disposition']
 
-        match = file.meta['content-disposition'].match(/filename=(?:"([^"]+)"|([^";]+))/)
+        match = headers['content-disposition'].match(/filename=(?:"([^"]+)"|([^";]+))/)
         return nil unless match
 
         match[1].presence || match[2].presence
       end
 
       def filename_from_uri
-        CGI.unescape(File.basename(file.base_uri.path))
+        CGI.unescape(File.basename(uri.path))
       end
 
       def method_missing(*args, &block)
         file.send(*args, &block)
       end
diff --git a/spec/downloader/base_spec.rb b/spec/downloader/base_spec.rb
index 3ca8a3c..337e4f4 100644
--- a/spec/downloader/base_spec.rb
+++ b/spec/downloader/base_spec.rb
@@ -24,25 +24,10 @@ describe CarrierWave::Downloader::Base do
     it "sets the filename to the file's decoded sanitized filename" do
       expect(remote_file.original_filename).to eq("#{filename}")
     end
   end
 
-  context "with a URL with internationalized domain name" do
-    let(:uri) { "http://ドメイン名例.jp/#{CGI.escape(filename)}" }
-    before do
-      stub_request(:get, 'http://xn--eckwd4c7cu47r2wf.jp/test.jpg').to_return(body: file)
-    end
-
-    it "converts to Punycode URI" do
-      expect(subject.process_uri(uri).to_s).to eq 'http://xn--eckwd4c7cu47r2wf.jp/test.jpg'
-    end
-
-    it "downloads a file" do
-      expect(subject.download(uri).file.read).to eq file
-    end
-  end
-
   context "with equal and colons in the query path" do
     let(:query) { 'test=query&with=equal&before=colon:param' }
     let(:uri) { "https://example.com/#{filename}?#{query}" }
     before do
       stub_request(:get, uri).to_return(body: file)
@@ -75,14 +60,10 @@ describe CarrierWave::Downloader::Base do
     it 'pass custom headers to request' do
       expect(subject.download(uri, { 'Authorization' => 'Bearer QWE' }).file.read).to eq file
     end
   end
 
-  it "raises an error when trying to download a local file" do
-    expect { subject.download('/etc/passwd') }.to raise_error(CarrierWave::DownloadError)
-  end
-
   context "with missing file" do
     before do
       stub_request(:get, uri).to_return(status: 404)
     end
 
@@ -93,10 +74,24 @@ describe CarrierWave::Downloader::Base do
     it "doesn't obscure original exception message" do
       expect { subject.download(uri) }.to raise_error(CarrierWave::DownloadError, /could not download file: 404/)
     end
   end
 
+  context "with server error" do
+    before do
+      stub_request(:get, uri).to_return(status: 500)
+    end
+
+    it "raises an error when trying to download" do
+      expect{ subject.download(uri) }.to raise_error(CarrierWave::DownloadError)
+    end
+
+    it "doesn't obscure original exception message" do
+      expect { subject.download(uri) }.to raise_error(CarrierWave::DownloadError, /could not download file: 500/)
+    end
+  end
+
   context "with a url that contains space" do
     let(:filename) { "my test.jpg" }
     before do
       stub_request(:get, uri).to_return(body: file)
     end
@@ -109,11 +104,11 @@ describe CarrierWave::Downloader::Base do
   context "with redirects" do
     let(:another_uri) { 'http://example.com/redirected.jpg' }
     before do
       stub_request(:get, uri).
         to_return(status: 301, body: "Redirecting", headers: { "Location" => another_uri })
-      stub_request(:get, another_uri).to_return(body: file)
+      stub_request(:get, /redirected.jpg/).to_return(body: file)
     end
 
     it "retrieves redirected file" do
       expect(subject.download(uri).file.read).to eq file
     end
@@ -121,11 +116,35 @@ describe CarrierWave::Downloader::Base do
     it "extracts filename from the url after redirection" do
       expect(subject.download(uri).original_filename).to eq 'redirected.jpg'
     end
   end
 
+  context "with SSRF prevention" do
+    before do
+      stub_request(:get, 'http://169.254.169.254/').to_return(body: file)
+      stub_request(:get, 'http://127.0.0.1/').to_return(body: file)
+    end
+
+    it "prevents accessing local files" do
+      expect { subject.download('/etc/passwd') }.to raise_error(CarrierWave::DownloadError)
+      expect { subject.download('file:///etc/passwd') }.to raise_error(CarrierWave::DownloadError)
+    end
+
+    it "prevents accessing internal addresses" do
+      expect { uploader.download!("http://169.254.169.254/") }.to raise_error CarrierWave::DownloadError
+      expect { uploader.download!("http://lvh.me/") }.to raise_error CarrierWave::DownloadError
+    end
+  end
+
   describe '#process_uri' do
+    it "converts a URL with internationalized domain name to Punycode URI" do
+      uri = "http://ドメイン名例.jp/#{CGI.escape(filename)}"
+      processed = subject.process_uri(uri)
+      expect(processed.class).to eq(URI::HTTP)
+      expect(processed.to_s).to eq 'http://xn--eckwd4c7cu47r2wf.jp/test.jpg'
+    end
+
     it "parses but not escape already escaped uris" do
       uri = 'http://example.com/%5B.jpg'
       processed = subject.process_uri(uri)
       expect(processed.class).to eq(URI::HTTP)
       expect(processed.to_s).to eq(uri)
@@ -176,6 +195,18 @@ describe CarrierWave::Downloader::Base do
     it "throws an exception on bad uris" do
       uri = '~http:'
       expect { subject.process_uri(uri) }.to raise_error(CarrierWave::DownloadError)
     end
   end
+
+  describe "#skip_ssrf_protection?" do
+    let(:uri) { 'http://localhost/test.jpg' }
+    before do
+      WebMock.stub_request(:get, uri).to_return(body: file)
+      allow(subject).to receive(:skip_ssrf_protection?).and_return(true)
+    end
+
+    it "allows local request to be made" do
+      expect(subject.download(uri).read).to eq 'this is stuff'
+    end
+  end
 end
diff --git a/spec/downloader/remote_file_spec.rb b/spec/downloader/remote_file_spec.rb
index 5ea9915..c61aae3 100644
--- a/spec/downloader/remote_file_spec.rb
+++ b/spec/downloader/remote_file_spec.rb
@@ -1,25 +1,63 @@
 require 'spec_helper'
 
 describe CarrierWave::Downloader::RemoteFile do
+  subject { CarrierWave::Downloader::RemoteFile.new(file) }
   let(:file) do
-    File.open(file_path("test.jpg")).tap { |f| OpenURI::Meta.init(f) }
+    Net::HTTPSuccess.new('1.0', '200', "").tap do |response|
+      response.body = File.read(file_path("test.jpg"))
+      response.instance_variable_set(:@read, true)
+      response.uri = URI.parse 'http://example.com/test'
+      response['content-type'] = 'image/jpeg'
+      response['vary'] = 'Accept-Encoding'
+    end
   end
-  subject { CarrierWave::Downloader::RemoteFile.new(file) }
 
-  before do
-    subject.base_uri = URI.parse 'http://example.com/test'
-    subject.meta_add_field 'content-type', 'image/jpeg'
+  context 'with Net::HTTPResponse instance' do
+    it 'returns content type' do
+      expect(subject.content_type).to eq 'image/jpeg'
+    end
+
+    it 'returns header' do
+      expect(subject.headers['vary']).to eq 'Accept-Encoding'
+    end
+
+    it 'returns URI' do
+      expect(subject.uri.to_s).to eq 'http://example.com/test'
+    end
   end
 
-  it 'sets file extension based on content-type if missing' do
-    expect(subject.original_filename).to eq "test.jpeg"
+  context 'with OpenURI::Meta instance' do
+    let(:file) do
+      File.open(file_path("test.jpg")).tap { |f| OpenURI::Meta.init(f) }.tap do |file|
+        file.base_uri = URI.parse 'http://example.com/test'
+        file.meta_add_field 'content-type', 'image/jpeg'
+        file.meta_add_field 'vary', 'Accept-Encoding'
+      end
+    end
+    it 'returns content type' do
+      expect(subject.content_type).to eq 'image/jpeg'
+    end
+
+    it 'returns header' do
+      expect(subject.headers['vary']).to eq 'Accept-Encoding'
+    end
+
+    it 'returns URI' do
+      expect(subject.uri.to_s).to eq 'http://example.com/test'
+    end
   end
 
-  describe 'with content-disposition' do
+
+  describe '#original_filename' do
+    let(:content_disposition){ nil }
     before do
-      subject.meta_add_field 'content-disposition', content_disposition
+      file['content-disposition'] = content_disposition if content_disposition
+    end
+
+    it 'sets file extension based on content-type if missing' do
+      expect(subject.original_filename).to eq "test.jpeg"
     end
 
     context 'when filename is quoted' do
       let(:content_disposition){ 'filename="another_test.jpg"' }
 
diff --git a/spec/mount_multiple_spec.rb b/spec/mount_multiple_spec.rb
index 6a679b0..9e71f8a 100644
--- a/spec/mount_multiple_spec.rb
+++ b/spec/mount_multiple_spec.rb
@@ -502,11 +502,11 @@ describe CarrierWave::Mount do
     end
 
     describe "#remote_images_urls" do
       subject { instance.remote_images_urls }
 
-      before { stub_request(:get, "www.example.com/#{test_file_name}").to_return(body: File.read(test_file_stub)) }
+      before { stub_request(:get, "http://www.example.com/#{test_file_name}").to_return(body: File.read(test_file_stub)) }
 
       context "returns nil" do
         it { is_expected.to be_nil }
       end
 
@@ -519,11 +519,11 @@ describe CarrierWave::Mount do
 
     describe "#remote_images_urls=" do
       subject(:images) { instance.images }
 
       before do
-        stub_request(:get, "www.example.com/#{test_file_name}").to_return(body: File.read(test_file_stub))
+        stub_request(:get, "http://www.example.com/#{test_file_name}").to_return(body: File.read(test_file_stub))
         stub_request(:get, "http://www.example.com/test.txt").to_return(status: 404)
         instance.remote_images_urls = remote_images_url
       end
 
       context "does nothing when nil is assigned" do
@@ -731,11 +731,11 @@ describe CarrierWave::Mount do
           end
         end
 
         context "when file was downloaded" do
           before do
-            stub_request(:get, "www.example.com/#{test_file_name}").to_return(body: File.read(test_file_stub))
+            stub_request(:get, "http://www.example.com/#{test_file_name}").to_return(body: File.read(test_file_stub))
             instance.remote_images_urls = ["http://www.example.com/#{test_file_name}"]
           end
 
           it { is_expected.to include(a_kind_of(CarrierWave::IntegrityError)) }
 
@@ -788,11 +788,11 @@ describe CarrierWave::Mount do
           it { is_expected.to include(a_kind_of(CarrierWave::ProcessingError)) }
         end
 
         context "when file was downloaded" do
           before do
-            stub_request(:get, "www.example.com/#{test_file_name}").to_return(body: File.read(test_file_stub))
+            stub_request(:get, "http://www.example.com/#{test_file_name}").to_return(body: File.read(test_file_stub))
             instance.remote_images_urls = ["http://www.example.com/#{test_file_name}"]
           end
 
           it { is_expected.to include(a_kind_of(CarrierWave::ProcessingError)) }
         end
@@ -801,12 +801,12 @@ describe CarrierWave::Mount do
 
     describe '#images_download_errors' do
       subject(:images_download_errors) { instance.images_download_errors }
 
       before do
-        stub_request(:get, "www.example.com/#{test_file_name}").to_return(body: File.read(test_file_stub))
-        stub_request(:get, "www.example.com/missing.jpg").to_return(status: 404)
+        stub_request(:get, "http://www.example.com/#{test_file_name}").to_return(body: File.read(test_file_stub))
+        stub_request(:get, "http://www.example.com/missing.jpg").to_return(status: 404)
       end
 
       describe "default behaviour" do
         it { expect(instance.images_download_errors).to be_empty }
       end
@@ -976,11 +976,11 @@ describe CarrierWave::Mount do
       it { expect(running { instance.images = [test_file_stub] }).to raise_error(CarrierWave::IntegrityError) }
     end
 
     context "when a downloaded image fails an integity check" do
       before do
-        stub_request(:get, "www.example.com/#{test_file_name}").to_return(body: test_file_stub)
+        stub_request(:get, "http://www.example.com/#{test_file_name}").to_return(body: test_file_stub)
       end
 
       it { expect(running {instance.remote_images_urls = ["http://www.example.com/#{test_file_name}"]}).to raise_error(CarrierWave::IntegrityError) }
     end
   end
@@ -1008,11 +1008,11 @@ describe CarrierWave::Mount do
       it { expect(running { instance.images = [test_file_stub] }).to raise_error(CarrierWave::ProcessingError) }
     end
 
     context "when a downloaded image fails an integity check" do
       before do
-        stub_request(:get, "www.example.com/#{test_file_name}").to_return(body: test_file_stub)
+        stub_request(:get, "http://www.example.com/#{test_file_name}").to_return(body: test_file_stub)
       end
 
       it { expect(running {instance.remote_images_urls = ["http://www.example.com/#{test_file_name}"]}).to raise_error(CarrierWave::ProcessingError) }
     end
   end
diff --git a/spec/mount_single_spec.rb b/spec/mount_single_spec.rb
index 1391b97..1743de9 100644
--- a/spec/mount_single_spec.rb
+++ b/spec/mount_single_spec.rb
@@ -297,11 +297,11 @@ describe CarrierWave::Mount do
       end
     end
 
     describe "#remote_image_url" do
       before do
-        stub_request(:get, "www.example.com/test.jpg").to_return(body: File.read(file_path("test.jpg")))
+        stub_request(:get, "http://www.example.com/test.jpg").to_return(body: File.read(file_path("test.jpg")))
       end
 
       it "returns nil" do
         expect(@instance.remote_image_url).to be_nil
       end
@@ -332,11 +332,11 @@ describe CarrierWave::Mount do
       end
     end
 
     describe "#remote_image_url=" do
       before do
-        stub_request(:get, "www.example.com/test.jpg").to_return(body: File.read(file_path("test.jpg")))
+        stub_request(:get, "http://www.example.com/test.jpg").to_return(body: File.read(file_path("test.jpg")))
       end
 
       it "does nothing when nil is assigned" do
         @instance.remote_image_url = nil
 
@@ -474,11 +474,11 @@ describe CarrierWave::Mount do
           expect(e).to be_an_instance_of(CarrierWave::IntegrityError)
           expect(e.message.lines.grep(/^You are not allowed to upload/)).to be_truthy
         end
 
         it "should be an error instance if file was downloaded" do
-          stub_request(:get, "www.example.com/test.jpg").to_return(body: File.read(file_path("test.jpg")))
+          stub_request(:get, "http://www.example.com/test.jpg").to_return(body: File.read(file_path("test.jpg")))
           @instance.remote_image_url = "http://www.example.com/test.jpg"
           e = @instance.image_integrity_error
 
           expect(e).to be_an_instance_of(CarrierWave::IntegrityError)
           expect(e.message.lines.grep(/^You are not allowed to upload/)).to be_truthy
@@ -519,22 +519,22 @@ describe CarrierWave::Mount do
           @instance.image = stub_file('test.jpg')
           expect(@instance.image_processing_error).to be_an_instance_of(CarrierWave::ProcessingError)
         end
 
         it "should be an error instance if file was downloaded" do
-          stub_request(:get, "www.example.com/test.jpg").to_return(body: File.read(file_path("test.jpg")))
+          stub_request(:get, "http://www.example.com/test.jpg").to_return(body: File.read(file_path("test.jpg")))
           @instance.remote_image_url = "http://www.example.com/test.jpg"
 
           expect(@instance.image_processing_error).to be_an_instance_of(CarrierWave::ProcessingError)
         end
       end
     end
 
     describe '#image_download_error' do
       before do
-        stub_request(:get, "www.example.com/test.jpg").to_return(body: File.read(file_path("test.jpg")))
-        stub_request(:get, "www.example.com/missing.jpg").to_return(status: 404)
+        stub_request(:get, "http://www.example.com/test.jpg").to_return(body: File.read(file_path("test.jpg")))
+        stub_request(:get, "http://www.example.com/missing.jpg").to_return(status: 404)
       end
 
       it "should be nil by default" do
         expect(@instance.image_download_error).to be_nil
       end
@@ -550,12 +550,12 @@ describe CarrierWave::Mount do
       end
     end
 
     describe '#image_download_error' do
       before do
-        stub_request(:get, "www.example.com/test.jpg").to_return(body: File.read(file_path("test.jpg")))
-        stub_request(:get, "www.example.com/missing.jpg").to_return(status: 404)
+        stub_request(:get, "http://www.example.com/test.jpg").to_return(body: File.read(file_path("test.jpg")))
+        stub_request(:get, "http://www.example.com/missing.jpg").to_return(status: 404)
       end
 
       it "should be nil by default" do
         expect(@instance.image_download_error).to be_nil
       end
@@ -706,11 +706,11 @@ describe CarrierWave::Mount do
         @instance.image = stub_file('test.jpg')
       }).to raise_error(CarrierWave::IntegrityError)
     end
 
     it "should raise an error if the image fails an integrity check when downloaded" do
-      stub_request(:get, "www.example.com/test.jpg").to_return(body: File.read(file_path("test.jpg")))
+      stub_request(:get, "http://www.example.com/test.jpg").to_return(body: File.read(file_path("test.jpg")))
 
       expect(running {
         @instance.remote_image_url = "http://www.example.com/test.jpg"
       }).to raise_error(CarrierWave::IntegrityError)
     end
@@ -740,11 +740,11 @@ describe CarrierWave::Mount do
         @instance.image = stub_file('test.jpg')
       }).to raise_error(CarrierWave::ProcessingError)
     end
 
     it "should raise an error if the image fails to be processed when downloaded" do
-      stub_request(:get, "www.example.com/test.jpg").to_return(body: File.read(file_path("test.jpg")))
+      stub_request(:get, "http://www.example.com/test.jpg").to_return(body: File.read(file_path("test.jpg")))
 
       expect(running {
         @instance.remote_image_url = "http://www.example.com/test.jpg"
       }).to raise_error(CarrierWave::ProcessingError)
     end
diff --git a/spec/orm/activerecord_spec.rb b/spec/orm/activerecord_spec.rb
index 339267a..c5e97d5 100644
--- a/spec/orm/activerecord_spec.rb
+++ b/spec/orm/activerecord_spec.rb
@@ -494,11 +494,11 @@ describe CarrierWave::ActiveRecord do
       end
     end
 
     describe "#remote_image_url=" do
       before do
-        stub_request(:get, "www.example.com/test.jpg").to_return(body: File.read(file_path("test.jpg")))
+        stub_request(:get, "http://www.example.com/test.jpg").to_return(body: File.read(file_path("test.jpg")))
       end
 
       # FIXME ideally image_changed? and remote_image_url_changed? would return true
       it "should mark image as changed when setting remote_image_url" do
         expect(@event.image_changed?).to be_falsey
@@ -1258,11 +1258,11 @@ describe CarrierWave::ActiveRecord do
       end
     end
 
     describe "#remote_images_urls=" do
       before do
-        stub_request(:get, "www.example.com/test.jpg").to_return(body: File.read(file_path("test.jpg")))
+        stub_request(:get, "http://www.example.com/test.jpg").to_return(body: File.read(file_path("test.jpg")))
       end
 
       # FIXME ideally images_changed? and remote_images_urls_changed? would return true
       it "should mark images as changed when setting remote_images_urls" do
         expect(@event.images_changed?).to be_falsey
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index f174720..b5eb5de 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -109,18 +109,27 @@ module CarrierWave
       def color_of_pixel(path, x, y)
         image = ::MiniMagick::Image.open(path)
         image.run_command("convert", "#{image.path}[1x1+#{x}+#{y}]", "-depth", "8", "txt:").split("\n")[1]
       end
     end
+
+    module SsrfProtectionAwareWebMock
+      def stub_request(method, uri)
+        uri = URI.parse(uri) if uri.is_a?(String)
+        uri.hostname = Resolv.getaddress(uri.hostname) if uri.is_a?(URI)
+        super
+      end
+    end
   end
 end
 
 RSpec.configure do |config|
   config.include CarrierWave::Test::Matchers
   config.include CarrierWave::Test::MockFiles
   config.include CarrierWave::Test::MockStorage
   config.include CarrierWave::Test::I18nHelpers
   config.include CarrierWave::Test::ManipulationHelpers
+  config.prepend CarrierWave::Test::SsrfProtectionAwareWebMock
   if RUBY_ENGINE == 'jruby'
     config.filter_run_excluding :rmagick => true
   end
 end
diff --git a/spec/uploader/download_spec.rb b/spec/uploader/download_spec.rb
index e3bfba2..ec29169 100644
--- a/spec/uploader/download_spec.rb
+++ b/spec/uploader/download_spec.rb
@@ -13,12 +13,12 @@ describe CarrierWave::Uploader::Download do
 
   describe '#download!' do
     before do
       allow(CarrierWave).to receive(:generate_cache_id).and_return(cache_id)
 
-      stub_request(:get, "www.example.com/#{test_file_name}")
-        .to_return(body: test_file)
+      stub_request(:get, "http://www.example.com/#{test_file_name}")
+        .to_return(body: test_file, headers: {'content-type': 'image/jpeg'})
     end
 
     context "when a file was downloaded" do
       before do
         uploader.download!(url)
@@ -46,10 +46,14 @@ describe CarrierWave::Uploader::Download do
       end
 
       it "sets the url" do
         expect(uploader.url).to eq("/uploads/tmp/#{cache_id}/#{test_file_name}")
       end
+
+      it "sets the content type" do
+        expect(uploader.content_type).to eq("image/jpeg")
+      end
     end
 
     context "with directory permissions set" do
       let(:permissions) { 0777 }
 

Vulnerable code:     stub_request(:get, "s3.amazonaws.com/Monkey/testfile.txt").
          file = OpenURI.open_uri(process_uri(url.to_s), headers)
        CarrierWave::Downloader::RemoteFile.new(file)
      attr_reader :file
        @file = file.is_a?(String) ? StringIO.new(file) : file
        mime_type = MiniMime.lookup_by_content_type(file.content_type)
        return nil unless file.meta.include? 'content-disposition'
        match = file.meta['content-disposition'].match(/filename=(?:"([^"]+)"|([^";]+))/)
        CGI.unescape(File.basename(file.base_uri.path))
  context "with a URL with internationalized domain name" do
    let(:uri) { "http://ドメイン名例.jp/#{CGI.escape(filename)}" }
    before do
      stub_request(:get, 'http://xn--eckwd4c7cu47r2wf.jp/test.jpg').to_return(body: file)
    end

    it "converts to Punycode URI" do
      expect(subject.process_uri(uri).to_s).to eq 'http://xn--eckwd4c7cu47r2wf.jp/test.jpg'
    end

    it "downloads a file" do
      expect(subject.download(uri).file.read).to eq file
    end
  end

  it "raises an error when trying to download a local file" do
    expect { subject.download('/etc/passwd') }.to raise_error(CarrierWave::DownloadError)
  end

      stub_request(:get, another_uri).to_return(body: file)
    File.open(file_path("test.jpg")).tap { |f| OpenURI::Meta.init(f) }
  subject { CarrierWave::Downloader::RemoteFile.new(file) }
  before do
    subject.base_uri = URI.parse 'http://example.com/test'
    subject.meta_add_field 'content-type', 'image/jpeg'
  it 'sets file extension based on content-type if missing' do
    expect(subject.original_filename).to eq "test.jpeg"
  describe 'with content-disposition' do
      subject.meta_add_field 'content-disposition', content_disposition
      before { stub_request(:get, "www.example.com/#{test_file_name}").to_return(body: File.read(test_file_stub)) }
        stub_request(:get, "www.example.com/#{test_file_name}").to_return(body: File.read(test_file_stub))
            stub_request(:get, "www.example.com/#{test_file_name}").to_return(body: File.read(test_file_stub))
            stub_request(:get, "www.example.com/#{test_file_name}").to_return(body: File.read(test_file_stub))
        stub_request(:get, "www.example.com/#{test_file_name}").to_return(body: File.read(test_file_stub))
        stub_request(:get, "www.example.com/missing.jpg").to_return(status: 404)
        stub_request(:get, "www.example.com/#{test_file_name}").to_return(body: test_file_stub)
        stub_request(:get, "www.example.com/#{test_file_name}").to_return(body: test_file_stub)
        stub_request(:get, "www.example.com/test.jpg").to_return(body: File.read(file_path("test.jpg")))
        stub_request(:get, "www.example.com/test.jpg").to_return(body: File.read(file_path("test.jpg")))
          stub_request(:get, "www.example.com/test.jpg").to_return(body: File.read(file_path("test.jpg")))
          stub_request(:get, "www.example.com/test.jpg").to_return(body: File.read(file_path("test.jpg")))
        stub_request(:get, "www.example.com/test.jpg").to_return(body: File.read(file_path("test.jpg")))
        stub_request(:get, "www.example.com/missing.jpg").to_return(status: 404)
        stub_request(:get, "www.example.com/test.jpg").to_return(body: File.read(file_path("test.jpg")))
        stub_request(:get, "www.example.com/missing.jpg").to_return(status: 404)
      stub_request(:get, "www.example.com/test.jpg").to_return(body: File.read(file_path("test.jpg")))
      stub_request(:get, "www.example.com/test.jpg").to_return(body: File.read(file_path("test.jpg")))
        stub_request(:get, "www.example.com/test.jpg").to_return(body: File.read(file_path("test.jpg")))
        stub_request(:get, "www.example.com/test.jpg").to_return(body: File.read(file_path("test.jpg")))
      stub_request(:get, "www.example.com/#{test_file_name}")
        .to_return(body: test_file)
Fixed code:   s.add_dependency "ssrf_filter", "~> 1.0"
    stub_request(:get, %r{/Monkey/testfile.txt}).
require 'ssrf_filter'
        uri = process_uri(url.to_s)
          if skip_ssrf_protection?(uri)
            response = OpenURI.open_uri(process_uri(url.to_s), headers)
          else
            request = nil
            response = SsrfFilter.get(uri, headers: headers) do |req|
              request = req
            end
            response.uri = request.uri
            response.value
          end
        CarrierWave::Downloader::RemoteFile.new(response)

      ##
      # If this returns true, SSRF protection will be bypassed.
      # You can override this if you want to allow accessing specific local URIs that are not SSRF exploitable.
      #
      # === Parameters
      #
      # [uri (URI)] The URI where the remote file is stored
      #
      # === Examples
      #
      #     class CarrierWave::Downloader::CustomDownloader < CarrierWave::Downloader::Base
      #       def skip_ssrf_protection?(uri)
      #         uri.hostname == 'localhost' && uri.port == 80
      #       end
      #     end
      #
      #     my_uploader.downloader = CarrierWave::Downloader::CustomDownloader
      #
      def skip_ssrf_protection?(uri)
        false
      end
      attr_reader :file, :uri
        case file
        when String
          @file = StringIO.new(file)
        when Net::HTTPResponse
          @file = StringIO.new(file.body)
          @content_type = file.content_type
          @headers = file
          @uri = file.uri
        else
          @file = file
          @content_type = file.content_type
          @headers = file.meta
          @uri = file.base_uri
        end
      end

      def content_type
        @content_type || 'application/octet-stream'
      end

      def headers
        @headers || {}
        mime_type = MiniMime.lookup_by_content_type(content_type)
        return nil unless headers['content-disposition']
        match = headers['content-disposition'].match(/filename=(?:"([^"]+)"|([^";]+))/)
        CGI.unescape(File.basename(uri.path))
  context "with server error" do
    before do
      stub_request(:get, uri).to_return(status: 500)
    end

    it "raises an error when trying to download" do
      expect{ subject.download(uri) }.to raise_error(CarrierWave::DownloadError)
    end

    it "doesn't obscure original exception message" do
      expect { subject.download(uri) }.to raise_error(CarrierWave::DownloadError, /could not download file: 500/)
    end
  end

      stub_request(:get, /redirected.jpg/).to_return(body: file)
  context "with SSRF prevention" do
    before do
      stub_request(:get, 'http://169.254.169.254/').to_return(body: file)
      stub_request(:get, 'http://127.0.0.1/').to_return(body: file)
    end

    it "prevents accessing local files" do
      expect { subject.download('/etc/passwd') }.to raise_error(CarrierWave::DownloadError)
      expect { subject.download('file:///etc/passwd') }.to raise_error(CarrierWave::DownloadError)
    end

    it "prevents accessing internal addresses" do
      expect { uploader.download!("http://169.254.169.254/") }.to raise_error CarrierWave::DownloadError
      expect { uploader.download!("http://lvh.me/") }.to raise_error CarrierWave::DownloadError
    end
  end

    it "converts a URL with internationalized domain name to Punycode URI" do
      uri = "http://ドメイン名例.jp/#{CGI.escape(filename)}"
      processed = subject.process_uri(uri)
      expect(processed.class).to eq(URI::HTTP)
      expect(processed.to_s).to eq 'http://xn--eckwd4c7cu47r2wf.jp/test.jpg'
    end


  describe "#skip_ssrf_protection?" do
    let(:uri) { 'http://localhost/test.jpg' }
    before do
      WebMock.stub_request(:get, uri).to_return(body: file)
      allow(subject).to receive(:skip_ssrf_protection?).and_return(true)
    end

    it "allows local request to be made" do
      expect(subject.download(uri).read).to eq 'this is stuff'
    end
  end
  subject { CarrierWave::Downloader::RemoteFile.new(file) }
    Net::HTTPSuccess.new('1.0', '200', "").tap do |response|
      response.body = File.read(file_path("test.jpg"))
      response.instance_variable_set(:@read, true)
      response.uri = URI.parse 'http://example.com/test'
      response['content-type'] = 'image/jpeg'
      response['vary'] = 'Accept-Encoding'
    end
  context 'with Net::HTTPResponse instance' do
    it 'returns content type' do
      expect(subject.content_type).to eq 'image/jpeg'
    end

    it 'returns header' do
      expect(subject.headers['vary']).to eq 'Accept-Encoding'
    end

    it 'returns URI' do
      expect(subject.uri.to_s).to eq 'http://example.com/test'
    end
  context 'with OpenURI::Meta instance' do
    let(:file) do
      File.open(file_path("test.jpg")).tap { |f| OpenURI::Meta.init(f) }.tap do |file|
        file.base_uri = URI.parse 'http://example.com/test'
        file.meta_add_field 'content-type', 'image/jpeg'
        file.meta_add_field 'vary', 'Accept-Encoding'
      end
    end
    it 'returns content type' do
      expect(subject.content_type).to eq 'image/jpeg'
    end

    it 'returns header' do
      expect(subject.headers['vary']).to eq 'Accept-Encoding'
    end

    it 'returns URI' do
      expect(subject.uri.to_s).to eq 'http://example.com/test'
    end

  describe '#original_filename' do
    let(:content_disposition){ nil }
      file['content-disposition'] = content_disposition if content_disposition
    end

    it 'sets file extension based on content-type if missing' do
      expect(subject.original_filename).to eq "test.jpeg"
      before { stub_request(:get, "http://www.example.com/#{test_file_name}").to_return(body: File.read(test_file_stub)) }
        stub_request(:get, "http://www.example.com/#{test_file_name}").to_return(body: File.read(test_file_stub))
            stub_request(:get, "http://www.example.com/#{test_file_name}").to_return(body: File.read(test_file_stub))
            stub_request(:get, "http://www.example.com/#{test_file_name}").to_return(body: File.read(test_file_stub))
        stub_request(:get, "http://www.example.com/#{test_file_name}").to_return(body: File.read(test_file_stub))
        stub_request(:get, "http://www.example.com/missing.jpg").to_return(status: 404)
        stub_request(:get, "http://www.example.com/#{test_file_name}").to_return(body: test_file_stub)
        stub_request(:get, "http://www.example.com/#{test_file_name}").to_return(body: test_file_stub)
        stub_request(:get, "http://www.example.com/test.jpg").to_return(body: File.read(file_path("test.jpg")))
        stub_request(:get, "http://www.example.com/test.jpg").to_return(body: File.read(file_path("test.jpg")))
          stub_request(:get, "http://www.example.com/test.jpg").to_return(body: File.read(file_path("test.jpg")))
          stub_request(:get, "http://www.example.com/test.jpg").to_return(body: File.read(file_path("test.jpg")))
        stub_request(:get, "http://www.example.com/test.jpg").to_return(body: File.read(file_path("test.jpg")))
        stub_request(:get, "http://www.example.com/missing.jpg").to_return(status: 404)
        stub_request(:get, "http://www.example.com/test.jpg").to_return(body: File.read(file_path("test.jpg")))
        stub_request(:get, "http://www.example.com/missing.jpg").to_return(status: 404)
      stub_request(:get, "http://www.example.com/test.jpg").to_return(body: File.read(file_path("test.jpg")))
      stub_request(:get, "http://www.example.com/test.jpg").to_return(body: File.read(file_path("test.jpg")))
        stub_request(:get, "http://www.example.com/test.jpg").to_return(body: File.read(file_path("test.jpg")))
        stub_request(:get, "http://www.example.com/test.jpg").to_return(body: File.read(file_path("test.jpg")))

    module SsrfProtectionAwareWebMock
      def stub_request(method, uri)
        uri = URI.parse(uri) if uri.is_a?(String)
        uri.hostname = Resolv.getaddress(uri.hostname) if uri.is_a?(URI)
        super
      end
    end
  config.prepend CarrierWave::Test::SsrfProtectionAwareWebMock
      stub_request(:get, "http://www.example.com/#{test_file_name}")
        .to_return(body: test_file, headers: {'content-type': 'image/jpeg'})

      it "sets the content type" do
        expect(uploader.content_type).to eq("image/jpeg")
      end