CVE-2017-0905: Server-Side Request Forgery (SSRF) (CWE-918)
value: The Recurly Client Ruby Library before 2.0.13, 2.1.11, 2.2.5, 2.3.10, 2.4.11, 2.5.4, 2.6.3, 2.7.8, 2.8.2, 2.9.2, 2.10.4, 2.11.3 is vulnerable to a Server-Side Request Forgery vulnerability in the "Resource#find" method that could result in compromise of API keys or other critical resources.
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: 9.8
Published: 2017-11-13T17:29Z
Repository: https://github.com/recurly/recurly-client-ruby
Language: Ruby
Files changed: lib/recurly/api.rb, lib/recurly/api/net_http_adapter.rb, lib/recurly/resource.rb
Commit date: 2017-11-08 16:13:33 -0700
Commit message: Fix SSRF vulnerability in Resource#find

Diff: diff --git a/lib/recurly/api.rb b/lib/recurly/api.rb
index 08408a5..00d9e42 100644
--- a/lib/recurly/api.rb
+++ b/lib/recurly/api.rb
@@ -13,10 +13,11 @@ module Recurly
   #   Recurly::API.delete 'accounts/1'        # => #<Net::HTTPNoContent ...>
   class API
     require 'recurly/api/errors'
 
     @@base_uri = "https://api.recurly.com/v2/"
+    @@valid_domains = [".recurly.com"]
 
     RECURLY_API_VERSION = '2.8'
 
     FORMATS = Helper.hash_with_indifferent_read_access(
       'pdf' => 'application/pdf',
@@ -73,10 +74,17 @@ module Recurly
       # @return [URI::Generic]
       def base_uri
         URI.parse @@base_uri.sub('api', Recurly.subdomain)
       end
 
+      def validate_uri!(uri)
+        domain = @@valid_domains.detect { |d| uri.host.end_with?(d) }
+        unless domain
+          raise ArgumentError, "URI #{uri} is invalid. You may only make requests to a Recurly domain."
+        end
+      end
+
       # @return [String]
       def user_agent
         "Recurly/#{Version}; #{RUBY_DESCRIPTION}"
       end
 
diff --git a/lib/recurly/api/net_http_adapter.rb b/lib/recurly/api/net_http_adapter.rb
index 39d34d8..7f7c849 100644
--- a/lib/recurly/api/net_http_adapter.rb
+++ b/lib/recurly/api/net_http_adapter.rb
@@ -41,10 +41,11 @@ module Recurly
             pairs = options[:params].map { |key, value|
               "#{CGI.escape key.to_s}=#{CGI.escape value.to_s}"
             }
             uri += "?#{pairs.join '&'}"
           end
+          self.validate_uri!(uri)
           request = METHODS[method].new uri.request_uri, head
           request.basic_auth(*[Recurly.api_key, nil].flatten[0, 2])
           if options[:body]
             request['Content-Type'] = content_type
             request.body = options[:body]
diff --git a/lib/recurly/resource.rb b/lib/recurly/resource.rb
index 1690164..9fe3b47 100644
--- a/lib/recurly/resource.rb
+++ b/lib/recurly/resource.rb
@@ -333,13 +333,12 @@ module Recurly
       def find(uuid, options = {})
         if uuid.nil? || uuid.to_s.empty?
           raise NotFound, "can't find a record with nil identifier"
         end
 
-        uri = uuid =~ /^http/ ? uuid : member_path(uuid)
         begin
-          from_response API.get(uri, {}, options)
+          from_response API.get(member_path(uuid), {}, options)
         rescue API::NotFound => e
           raise NotFound, e.description
         end
       end
 

Vulnerable code:         uri = uuid =~ /^http/ ? uuid : member_path(uuid)
          from_response API.get(uri, {}, options)
Fixed code:     @@valid_domains = [".recurly.com"]
      def validate_uri!(uri)
        domain = @@valid_domains.detect { |d| uri.host.end_with?(d) }
        unless domain
          raise ArgumentError, "URI #{uri} is invalid. You may only make requests to a Recurly domain."
        end
      end

          self.validate_uri!(uri)
          from_response API.get(member_path(uuid), {}, options)