GeoServer Stored Cross-Site Scripting (XSS) vulnerability in WMS OpenLayers Format
Description
GeoServer is an open source software server written in Java that allows users to share and edit geospatial data. A stored cross-site scripting (XSS) vulnerability exists in versions prior to 2.23.3 and 2.24.1 that enables an authenticated administrator with workspace-level privileges to store a JavaScript payload in the GeoServer catalog that will execute in the context of another user's browser when viewed in the WMS GetMap OpenLayers Output Format. Access to the WMS OpenLayers Format is available to all users by default although data and service security may limit users' ability to trigger the XSS. Versions 2.23.3 and 2.24.1 contain a patch for this issue.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.geoserver:gs-wmsMaven | < 2.23.3 | 2.23.3 |
org.geoserver:gs-wmsMaven | >= 2.24.0, < 2.24.1 | 2.24.1 |
Affected products
1Patches
2a26c32a469ee[GEOS-11153] Improve handling special characters in the WMS OpenLayers Format (#7193)
4 files changed · +48 −33
src/wms/src/main/java/org/geoserver/wms/map/AbstractOpenLayersMapOutputFormat.java+2 −0 modified@@ -7,6 +7,7 @@ import static org.geoserver.template.TemplateUtils.FM_VERSION; +import freemarker.core.HTMLOutputFormat; import freemarker.ext.beans.BeansWrapper; import freemarker.template.Configuration; import freemarker.template.Template; @@ -107,6 +108,7 @@ public abstract class AbstractOpenLayersMapOutputFormat implements GetMapOutputF BeansWrapper bw = new BeansWrapper(FM_VERSION); bw.setExposureLevel(BeansWrapper.EXPOSE_PROPERTIES_ONLY); cfg.setObjectWrapper(bw); + cfg.setOutputFormat(HTMLOutputFormat.INSTANCE); } /** wms configuration */
src/wms/src/main/resources/org/geoserver/wms/map/OpenLayers2MapTemplate.ftl+5 −3 modified@@ -106,6 +106,7 @@ <script src="${relBaseUrl}/openlayers/OpenLayers.js" type="text/javascript"> </script> <script defer="defer" type="text/javascript"> + <#outputformat "JavaScript"> var map; var untiled; var tiled; @@ -149,7 +150,7 @@ // setup tiled layer tiled = new OpenLayers.Layer.WMS( - "${layerName} - Tiled", "${baseUrl}/${servicePath}", + "${layerName?js_string} - Tiled", "${baseUrl}/${servicePath?js_string}", { <#list parameters as param> "${param.name?js_string}": '${param.value?js_string}', @@ -168,7 +169,7 @@ // setup single tiled layer untiled = new OpenLayers.Layer.WMS( - "${layerName} - Untiled", "${baseUrl}/${servicePath}", + "${layerName?js_string} - Untiled", "${baseUrl}/${servicePath?js_string}", { <#list parameters as param> "${param.name?js_string}": '${param.value?js_string}', @@ -242,7 +243,7 @@ if(map.layers[0].params.FEATUREID) { params.featureid = map.layers[0].params.FEATUREID; } - OpenLayers.loadURL("${baseUrl}/${servicePath}", params, this, setHTML, setHTML); + OpenLayers.loadURL("${baseUrl}/${servicePath?js_string}", params, this, setHTML, setHTML); OpenLayers.Event.stop(e); }); } @@ -447,6 +448,7 @@ tiled.mergeNewParams(params); untiled.mergeNewParams(params); } + </#outputformat> </script> </head> <body onload="init()">
src/wms/src/main/resources/org/geoserver/wms/map/OpenLayers3MapTemplate.ftl+4 −3 modified@@ -228,6 +228,7 @@ <em>Click on the map to get feature info</em> </div> <script type="text/javascript"> + <#outputformat "JavaScript"> var pureCoverage = ${pureCoverage?string}; // if this is just a coverage or a group of them, disable a few items, // and default to jpeg format @@ -257,7 +258,7 @@ var untiled = new ol.layer.Image({ source: new ol.source.ImageWMS({ ratio: 1, - url: '${baseUrl}/${servicePath}', + url: '${baseUrl}/${servicePath?js_string}', params: {'FORMAT': format, 'VERSION': '1.1.1', <#list parameters as param> @@ -269,7 +270,7 @@ var tiled = new ol.layer.Tile({ visible: false, source: new ol.source.TileWMS({ - url: '${baseUrl}/${servicePath}', + url: '${baseUrl}/${servicePath?js_string}', params: {'FORMAT': format, 'VERSION': '1.1.1', tiled: true, @@ -450,7 +451,7 @@ } map.updateSize() } - + </#outputformat> </script> </body> </html>
src/wms/src/test/java/org/geoserver/wms/map/OpenLayersMapOutputFormatTest.java+37 −27 modified@@ -130,22 +130,28 @@ public void testXssFix() throws Exception { StyleInfo styleByName = catalog.getStyleByName("Default"); Style basicStyle = styleByName.getStyle(); FeatureLayer layer = new FeatureLayer(fs, basicStyle); - layer.setTitle("Title"); + layer.setTitle("Title</foo"); map.addLayer(layer); request.setFormat("application/openlayers"); - String htmlDoc = getAsHTML(map); - // check that weird param is correctly encoded to avoid js code execution - int index = - htmlDoc.replace("\\n", "") - .replace("\\r", "") - .indexOf( - "\"<\\/script><script>alert(\\'x-scripted\\');<\\/script><script>\": 'foo'"); - assertTrue(index > -1); - index = - htmlDoc.replace("\\n", "") - .replace("\\r", "") - .indexOf("\"25064;ALERT(1)//419\": '1'"); - assertTrue(index > -1); + + StyleInfo otherStyle = new StyleInfoImpl(null); + otherStyle.setName("style<>"); + try { + request.getLayers().get(0).getLayerInfo().getStyles().add(otherStyle); + String htmlDoc = getAsHTML(map); + // check that weird param is correctly encoded to avoid js code execution + assertThat( + htmlDoc, + containsString( + "\"<\\/script><script>alert(\\'x-scripted\\');<\\/script><script>\": 'foo'")); + assertThat(htmlDoc, containsString("\"25064;ALERT(1)//419\": '1'")); + assertThat(htmlDoc, not(containsString(layer.getTitle()))); + assertThat(htmlDoc, containsString("Title<\\/foo")); + assertThat(htmlDoc, not(containsString(otherStyle.getName()))); + assertThat(htmlDoc, containsString("style<>")); + } finally { + request.getLayers().get(0).getLayerInfo().getStyles().remove(otherStyle); + } } @Test @@ -472,19 +478,23 @@ public void testXssOL3() throws Exception { layer.setTitle("Title"); map.addLayer(layer); request.setFormat("application/openlayers3"); - String htmlDoc = getAsHTMLOL3(map); - // check that weird param is correctly encoded to avoid js code execution - int index = - htmlDoc.replace("\\n", "") - .replace("\\r", "") - .indexOf( - "\"<\\/script><script>alert(\\'x-scripted\\');<\\/script><script>\": 'foo'"); - assertTrue(index > -1); - index = - htmlDoc.replace("\\n", "") - .replace("\\r", "") - .indexOf("\"25064;ALERT(1)//419\": '1'"); - assertTrue(index > -1); + + StyleInfo otherStyle = new StyleInfoImpl(null); + otherStyle.setName("style<>"); + try { + request.getLayers().get(0).getLayerInfo().getStyles().add(otherStyle); + String htmlDoc = getAsHTMLOL3(map); + // check that weird param is correctly encoded to avoid js code execution + assertThat( + htmlDoc, + containsString( + "\"<\\/script><script>alert(\\'x-scripted\\');<\\/script><script>\": 'foo'")); + assertThat(htmlDoc, containsString("\"25064;ALERT(1)//419\": '1'")); + assertThat(htmlDoc, not(containsString(otherStyle.getName()))); + assertThat(htmlDoc, containsString("style<>")); + } finally { + request.getLayers().get(0).getLayerInfo().getStyles().remove(otherStyle); + } } @Test
4557a832eed1[GEOS-11153] Improve handling special characters in the WMS OpenLayers Format (#7194)
4 files changed · +48 −33
src/wms/src/main/java/org/geoserver/wms/map/AbstractOpenLayersMapOutputFormat.java+2 −0 modified@@ -7,6 +7,7 @@ import static org.geoserver.template.TemplateUtils.FM_VERSION; +import freemarker.core.HTMLOutputFormat; import freemarker.ext.beans.BeansWrapper; import freemarker.template.Configuration; import freemarker.template.Template; @@ -107,6 +108,7 @@ public abstract class AbstractOpenLayersMapOutputFormat implements GetMapOutputF BeansWrapper bw = new BeansWrapper(FM_VERSION); bw.setExposureLevel(BeansWrapper.EXPOSE_PROPERTIES_ONLY); cfg.setObjectWrapper(bw); + cfg.setOutputFormat(HTMLOutputFormat.INSTANCE); } /** wms configuration */
src/wms/src/main/resources/org/geoserver/wms/map/OpenLayers2MapTemplate.ftl+5 −3 modified@@ -106,6 +106,7 @@ <script src="${relBaseUrl}/openlayers/OpenLayers.js" type="text/javascript"> </script> <script defer="defer" type="text/javascript"> + <#outputformat "JavaScript"> var map; var untiled; var tiled; @@ -149,7 +150,7 @@ // setup tiled layer tiled = new OpenLayers.Layer.WMS( - "${layerName} - Tiled", "${baseUrl}/${servicePath}", + "${layerName?js_string} - Tiled", "${baseUrl}/${servicePath?js_string}", { <#list parameters as param> "${param.name?js_string}": '${param.value?js_string}', @@ -168,7 +169,7 @@ // setup single tiled layer untiled = new OpenLayers.Layer.WMS( - "${layerName} - Untiled", "${baseUrl}/${servicePath}", + "${layerName?js_string} - Untiled", "${baseUrl}/${servicePath?js_string}", { <#list parameters as param> "${param.name?js_string}": '${param.value?js_string}', @@ -242,7 +243,7 @@ if(map.layers[0].params.FEATUREID) { params.featureid = map.layers[0].params.FEATUREID; } - OpenLayers.loadURL("${baseUrl}/${servicePath}", params, this, setHTML, setHTML); + OpenLayers.loadURL("${baseUrl}/${servicePath?js_string}", params, this, setHTML, setHTML); OpenLayers.Event.stop(e); }); } @@ -447,6 +448,7 @@ tiled.mergeNewParams(params); untiled.mergeNewParams(params); } + </#outputformat> </script> </head> <body onload="init()">
src/wms/src/main/resources/org/geoserver/wms/map/OpenLayers3MapTemplate.ftl+4 −3 modified@@ -228,6 +228,7 @@ <em>Click on the map to get feature info</em> </div> <script type="text/javascript"> + <#outputformat "JavaScript"> var pureCoverage = ${pureCoverage?string}; // if this is just a coverage or a group of them, disable a few items, // and default to jpeg format @@ -257,7 +258,7 @@ var untiled = new ol.layer.Image({ source: new ol.source.ImageWMS({ ratio: 1, - url: '${baseUrl}/${servicePath}', + url: '${baseUrl}/${servicePath?js_string}', params: {'FORMAT': format, 'VERSION': '1.1.1', <#list parameters as param> @@ -269,7 +270,7 @@ var tiled = new ol.layer.Tile({ visible: false, source: new ol.source.TileWMS({ - url: '${baseUrl}/${servicePath}', + url: '${baseUrl}/${servicePath?js_string}', params: {'FORMAT': format, 'VERSION': '1.1.1', tiled: true, @@ -450,7 +451,7 @@ } map.updateSize() } - + </#outputformat> </script> </body> </html>
src/wms/src/test/java/org/geoserver/wms/map/OpenLayersMapOutputFormatTest.java+37 −27 modified@@ -130,22 +130,28 @@ public void testXssFix() throws Exception { StyleInfo styleByName = catalog.getStyleByName("Default"); Style basicStyle = styleByName.getStyle(); FeatureLayer layer = new FeatureLayer(fs, basicStyle); - layer.setTitle("Title"); + layer.setTitle("Title</foo"); map.addLayer(layer); request.setFormat("application/openlayers"); - String htmlDoc = getAsHTML(map); - // check that weird param is correctly encoded to avoid js code execution - int index = - htmlDoc.replace("\\n", "") - .replace("\\r", "") - .indexOf( - "\"<\\/script><script>alert(\\'x-scripted\\');<\\/script><script>\": 'foo'"); - assertTrue(index > -1); - index = - htmlDoc.replace("\\n", "") - .replace("\\r", "") - .indexOf("\"25064;ALERT(1)//419\": '1'"); - assertTrue(index > -1); + + StyleInfo otherStyle = new StyleInfoImpl(null); + otherStyle.setName("style<>"); + try { + request.getLayers().get(0).getLayerInfo().getStyles().add(otherStyle); + String htmlDoc = getAsHTML(map); + // check that weird param is correctly encoded to avoid js code execution + assertThat( + htmlDoc, + containsString( + "\"<\\/script><script>alert(\\'x-scripted\\');<\\/script><script>\": 'foo'")); + assertThat(htmlDoc, containsString("\"25064;ALERT(1)//419\": '1'")); + assertThat(htmlDoc, not(containsString(layer.getTitle()))); + assertThat(htmlDoc, containsString("Title<\\/foo")); + assertThat(htmlDoc, not(containsString(otherStyle.getName()))); + assertThat(htmlDoc, containsString("style<>")); + } finally { + request.getLayers().get(0).getLayerInfo().getStyles().remove(otherStyle); + } } @Test @@ -472,19 +478,23 @@ public void testXssOL3() throws Exception { layer.setTitle("Title"); map.addLayer(layer); request.setFormat("application/openlayers3"); - String htmlDoc = getAsHTMLOL3(map); - // check that weird param is correctly encoded to avoid js code execution - int index = - htmlDoc.replace("\\n", "") - .replace("\\r", "") - .indexOf( - "\"<\\/script><script>alert(\\'x-scripted\\');<\\/script><script>\": 'foo'"); - assertTrue(index > -1); - index = - htmlDoc.replace("\\n", "") - .replace("\\r", "") - .indexOf("\"25064;ALERT(1)//419\": '1'"); - assertTrue(index > -1); + + StyleInfo otherStyle = new StyleInfoImpl(null); + otherStyle.setName("style<>"); + try { + request.getLayers().get(0).getLayerInfo().getStyles().add(otherStyle); + String htmlDoc = getAsHTMLOL3(map); + // check that weird param is correctly encoded to avoid js code execution + assertThat( + htmlDoc, + containsString( + "\"<\\/script><script>alert(\\'x-scripted\\');<\\/script><script>\": 'foo'")); + assertThat(htmlDoc, containsString("\"25064;ALERT(1)//419\": '1'")); + assertThat(htmlDoc, not(containsString(otherStyle.getName()))); + assertThat(htmlDoc, containsString("style<>")); + } finally { + request.getLayers().get(0).getLayerInfo().getStyles().remove(otherStyle); + } } @Test
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
7- github.com/advisories/GHSA-fcpm-hchj-mh72ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-23818ghsaADVISORY
- github.com/geoserver/geoserver/commit/4557a832eed19ec18b9753cb97e8aa85269741d2ghsax_refsource_MISCWEB
- github.com/geoserver/geoserver/commit/a26c32a469ee4c599236380452ffb4260361bd6fghsax_refsource_MISCWEB
- github.com/geoserver/geoserver/pull/7174ghsax_refsource_MISCWEB
- github.com/geoserver/geoserver/security/advisories/GHSA-fcpm-hchj-mh72ghsax_refsource_CONFIRMWEB
- osgeo-org.atlassian.net/browse/GEOS-11153ghsax_refsource_MISCWEB
News mentions
0No linked articles in our index yet.