CVE-2017-0906: Server-Side Request Forgery (SSRF) (CWE-918)
value: The Recurly Client Python Library before 2.0.5, 2.1.16, 2.2.22, 2.3.1, 2.4.5, 2.5.1, 2.6.2 is vulnerable to a Server-Side Request Forgery vulnerability in the "Resource.get" 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-python
Language: Python
Files changed: recurly/resource.py
Commit date: 2017-11-08 18:45:28 -0600
Commit message: Fix SSRF: do not use urljoin, quote uuids

Diff: diff --git a/recurly/resource.py b/recurly/resource.py
index 51fbf96..b0d214a 100644
--- a/recurly/resource.py
+++ b/recurly/resource.py
@@ -10,12 +10,11 @@ import six
 
 import recurly
 import recurly.errors
 from recurly.link_header import parse_link_value
 from six.moves import http_client
-from six.moves.urllib.parse import urlencode, urljoin, urlsplit
-
+from six.moves.urllib.parse import urlencode, urlsplit, quote
 
 class Money(object):
 
     """An amount of money in one or more currencies."""
 
@@ -336,11 +335,12 @@ class Resource(object):
 
         Only `Resource` classes with specified `member_path` attributes
         can be directly requested with this method.
 
         """
-        url = urljoin(recurly.base_uri(), cls.member_path % (uuid,))
+        uuid = quote(str(uuid))
+        url = recurly.base_uri() + (cls.member_path % (uuid,))
         resp, elem = cls.element_for_url(url)
         return cls.from_element(elem)
 
     @classmethod
     def headers_for_url(cls, url):
@@ -604,21 +604,21 @@ class Resource(object):
         endpoints can be requested with this method. Any provided
         keyword arguments are passed to the API endpoint as query
         parameters.
 
         """
-        url = urljoin(recurly.base_uri(), cls.collection_path)
+        url = recurly.base_uri() + cls.collection_path
         if kwargs:
             url = '%s?%s' % (url, urlencode(kwargs))
         return Page.page_for_url(url)
 
     @classmethod
     def count(cls, **kwargs):
         """Return a count of server side resources given
         filtering arguments in kwargs.
         """
-        url = urljoin(recurly.base_uri(), cls.collection_path)
+        url = recurly.base_uri() + cls.collection_path
         if kwargs:
             url = '%s?%s' % (url, urlencode(kwargs))
         return Page.count_for_url(url)
 
     def save(self):
@@ -636,11 +636,11 @@ class Resource(object):
 
     def _update(self):
         return self.put(self._url)
 
     def _create(self):
-        url = urljoin(recurly.base_uri(), self.collection_path)
+        url = recurly.base_uri() + self.collection_path
         return self.post(url)
 
     def put(self, url):
         """Sends this `Resource` instance to the service with a
         ``PUT`` request to the given URL."""

Vulnerable code: from six.moves.urllib.parse import urlencode, urljoin, urlsplit

        url = urljoin(recurly.base_uri(), cls.member_path % (uuid,))
        url = urljoin(recurly.base_uri(), cls.collection_path)
        url = urljoin(recurly.base_uri(), cls.collection_path)
        url = urljoin(recurly.base_uri(), self.collection_path)
Fixed code: from six.moves.urllib.parse import urlencode, urlsplit, quote
        uuid = quote(str(uuid))
        url = recurly.base_uri() + (cls.member_path % (uuid,))
        url = recurly.base_uri() + cls.collection_path
        url = recurly.base_uri() + cls.collection_path
        url = recurly.base_uri() + self.collection_path