CVE-2023-42439: Server-Side Request Forgery (SSRF) (CWE-918)
value: GeoNode is an open source platform that facilitates the creation, sharing, and collaborative use of geospatial data. A SSRF vulnerability exists starting in version 3.2.0, bypassing existing controls on the software. This can allow a user to request internal services for a full read SSRF, returning any data from the internal network. The application is using a whitelist, but the whitelist can be bypassed. The bypass will trick the application that the first host is a whitelisted address, but the browser will use `@` or `%40` as a credential to the host geoserver on port 8080, this will return the data to that host on the response. Version 4.1.3.post1 is the first available version that contains a patch.
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: 6.5
Published: 2023-09-15T21:15Z
Repository: https://github.com/GeoNode/geonode
Language: Python
Files changed: geonode/proxy/tests.py, geonode/utils.py
Commit date: 2023-09-14 13:17:12 +0200
Commit message: Merge pull request from GHSA-pxg5-h34r-7q8p

* fixes advisory

* - fix tests
Diff: diff --git a/geonode/proxy/tests.py b/geonode/proxy/tests.py
index f44315c52a..807a64f3f7 100644
--- a/geonode/proxy/tests.py
+++ b/geonode/proxy/tests.py
@@ -211,10 +211,20 @@ class ProxyTest(GeoNodeBaseTestSupport):
         url = f"http://125.126.127.128\\@%23{urlsplit(settings.SITEURL).hostname}"
 
         response = self.client.get(f"{self.proxy_url}?url={url}")
         self.assertEqual(response.status_code, 403)
 
+        url = f"{settings.SITEURL.rstrip('/')}@geoserver:8080/web/index.html"
+
+        response = self.client.get(f"{self.proxy_url}?url={url}")
+        self.assertEqual(response.status_code, 403)
+
+        url = f"{settings.SITEURL.rstrip('/')}%40geoserver:8080/web/index.html"
+
+        response = self.client.get(f"{self.proxy_url}?url={url}")
+        self.assertEqual(response.status_code, 403)
+
         # Legit requests using the local host (SITEURL)
         url = f"/\\@%23{urlsplit(settings.SITEURL).hostname}"
 
         response = self.client.get(f"{self.proxy_url}?url={url}")
         self.assertEqual(response.status_code, 200)
diff --git a/geonode/utils.py b/geonode/utils.py
index d9c6734534..d15dafe316 100755
--- a/geonode/utils.py
+++ b/geonode/utils.py
@@ -86,10 +86,11 @@ from urllib.parse import (
     urljoin,
     unquote,
     urlparse,
     urlsplit,
     urlencode,
+    urlunparse,
     parse_qsl,
     ParseResult,
 )
 
 MAX_EXTENT = 20037508.34
@@ -1905,24 +1906,40 @@ def build_absolute_uri(url):
     if url and "http" not in url:
         url = urljoin(settings.SITEURL, url)
     return url
 
 
+def remove_credentials_from_url(url):
+    # Parse the URL
+    parsed_url = urlparse(url)
+
+    # Remove the username and password from the parsed URL
+    parsed_url = parsed_url._replace(netloc=parsed_url.netloc.split("@")[-1])
+
+    # Reconstruct the URL without credentials
+    cleaned_url = urlunparse(parsed_url)
+
+    return cleaned_url
+
+
 def extract_ip_or_domain(url):
+    # Decode the URL to handle percent-encoded characters
+    _url = remove_credentials_from_url(unquote(url))
+
     ip_regex = re.compile("^(?:http://|https://)(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})")
     domain_regex = re.compile("^(?:http://|https://)([a-zA-Z0-9.-]+)")
 
-    match = ip_regex.findall(url)
+    match = ip_regex.findall(_url)
     if len(match):
         ip_address = match[0]
         try:
             ipaddress.ip_address(ip_address)  # Validate the IP address
             return ip_address
         except ValueError:
             pass
 
-    match = domain_regex.findall(url)
+    match = domain_regex.findall(_url)
     if len(match):
         return match[0]
 
     return None
 

Vulnerable code:     match = ip_regex.findall(url)
    match = domain_regex.findall(url)
Fixed code:         url = f"{settings.SITEURL.rstrip('/')}@geoserver:8080/web/index.html"

        response = self.client.get(f"{self.proxy_url}?url={url}")
        self.assertEqual(response.status_code, 403)

        url = f"{settings.SITEURL.rstrip('/')}%40geoserver:8080/web/index.html"

        response = self.client.get(f"{self.proxy_url}?url={url}")
        self.assertEqual(response.status_code, 403)

    urlunparse,
def remove_credentials_from_url(url):
    # Parse the URL
    parsed_url = urlparse(url)

    # Remove the username and password from the parsed URL
    parsed_url = parsed_url._replace(netloc=parsed_url.netloc.split("@")[-1])

    # Reconstruct the URL without credentials
    cleaned_url = urlunparse(parsed_url)

    return cleaned_url


    # Decode the URL to handle percent-encoded characters
    _url = remove_credentials_from_url(unquote(url))

    match = ip_regex.findall(_url)
    match = domain_regex.findall(_url)