CVE-2023-40017 (Python, published 2023-08-24T23:15Z)
Description: value: GeoNode is an open source platform that facilitates the creation, sharing, and collaborative use of geospatial data. In versions 3.2.0 through 4.1.2, the endpoint `/proxy/?url=` does not properly protect against server-side request forgery. This allows an attacker to port scan internal hosts and request information from internal hosts. A patch is available at commit a9eebae80cb362009660a1fd49e105e7cdb499b9.
Severity: nan (CVSS v3 base score: 7.5)
Weakness: CWE-918 \u2014 Server-Side Request Forgery (SSRF)
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.
Repository: https://github.com/GeoNode/geonode
Files changed:
geonode/proxy/tests.py
geonode/proxy/views.py
geonode/utils.py
requirements.txt
setup.cfg
Commit message: Merge pull request from GHSA-rmxg-6qqf-x8mr

* - Fixes "Server Side Request forgery"

* Fixes IP or domain extraction

* fixed formatting

* - Bump ipaddress to version 1.0.23

---------

Co-authored-by: G. Allegri <giohappy@gmail.com>

Vulnerable code:
from geonode.utils import resolve_object, check_ogc_backend, get_headers, http_client, json_response
        if not validate_host(url.hostname, PROXY_ALLOWED_HOSTS):

Fixed code:
    def test_proxy_url_forgery(self):
        """The GeoNode Proxy should preserve the original request headers."""
        import geonode.proxy.views
        from urllib.parse import urlsplit

        class Response:
            status_code = 200
            content = "Hello World"
            headers = {
                "Content-Type": "text/plain",
                "Vary": "Authorization, Accept-Language, Cookie, origin",
                "X-Content-Type-Options": "nosniff",
                "X-XSS-Protection": "1; mode=block",
                "Referrer-Policy": "same-origin",
                "X-Frame-Options": "SAMEORIGIN",
                "Content-Language": "en-us",
                "Content-Length": "119",
                "Content-Disposition": 'attachment; filename="filename.tif"',
            }

        request_mock = MagicMock()
        request_mock.return_value = (Response(), None)

        # Non-Legit requests attempting SSRF
        geonode.proxy.views.http_client.request = request_mock
        url = f"http://example.org\@%23{urlsplit(settings.SITEURL).hostname}"

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

        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)

        # 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)

        url = f"{settings.SITEURL}\@%23{urlsplit(settings.SITEURL).hostname}"

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

from geonode.utils import (
    resolve_object,
    check_ogc_backend,
    get_headers,
    http_client,
    json_response,
    extract_ip_or_domain,
)
        if not validate_host(extract_ip_or_domain(raw_url), PROXY_ALLOWED_HOSTS):
import ipaddress
def extract_ip_or_domain(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)
    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)
    if len(match):
        return match[0]

    return None


ipaddress==1.0.23
    ipaddress==1.0.23