GeoServer Stored Cross-Site Scripting (XSS) vulnerability in GWC Seed Form
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.2 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 administrator’s browser when viewed in the GWC Seed Form. Access to the GWC Seed Form is limited to full administrators by default and granting non-administrators access to this endpoint is not recommended. Versions 2.23.2 and 2.24.1 contain a fix for this issue.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.geoserver:gs-gwc-restMaven | < 2.23.2 | 2.23.2 |
org.geoserver:gs-gwc-restMaven | >= 2.24.0, < 2.24.1 | 2.24.1 |
Affected products
1Patches
2c0ca08a20bc0[GWC-1172] Improve handling special characters in the GWC Seed Form
2 files changed · +66 −17
geowebcache/rest/src/main/java/org/geowebcache/rest/service/FormService.java+24 −17 modified@@ -18,6 +18,7 @@ */ package org.geowebcache.rest.service; +import static org.apache.commons.text.StringEscapeUtils.escapeHtml4; import static org.geowebcache.seed.TileBreeder.TILE_FAILURE_RETRY_COUNT_DEFAULT; import static org.geowebcache.seed.TileBreeder.TILE_FAILURE_RETRY_WAIT_TIME_DEFAULT; import static org.geowebcache.seed.TileBreeder.TOTAL_FAILURES_BEFORE_ABORTING_DEFAULT; @@ -356,7 +357,10 @@ private void makeModifiableParameters(StringBuilder doc, TileLayer tl) { String key = pf.getKey(); String defaultValue = pf.getDefaultValue(); List<String> legalValues = pf.getLegalValues(); - doc.append("<tr><td>").append(key.toUpperCase()).append(": ").append("</td><td>"); + doc.append("<tr><td>") + .append(escapeHtml4(key.toUpperCase())) + .append(": ") + .append("</td><td>"); String parameterId = "parameter_" + key; if (pf instanceof StringParameterFilter) { Map<String, String> keysValues = makeParametersMap(defaultValue, legalValues); @@ -487,7 +491,7 @@ private void makeBboxHints(StringBuilder doc, TileLayer tl) { GridSubset subset = tl.getGridSubset(gridSetId); doc.append( "<li>" - + gridSetId + + escapeHtml4(gridSetId) + ": " + subset.getOriginalExtent().toString() + "</li>\n"); @@ -501,11 +505,11 @@ private void makeTextInput(StringBuilder doc, String id, int size) { private void makeTextInput(StringBuilder doc, String id, int size, String defaultValue) { doc.append( "<input name=\"" - + id + + escapeHtml4(id) + "\" type=\"text\" size=\"" + size + "\" value=\"" - + defaultValue + + escapeHtml4(defaultValue) + "\"/>"); } @@ -603,7 +607,7 @@ private void makeGridSetPulldown(StringBuilder doc, TileLayer tl) { private void makePullDown( StringBuilder doc, String id, Map<String, String> keysValues, String defaultKey) { - doc.append("<select name=\"" + id + "\">\n"); + doc.append("<select name=\"" + escapeHtml4(id) + "\">\n"); Iterator<Map.Entry<String, String>> iter = keysValues.entrySet().iterator(); @@ -612,16 +616,16 @@ private void makePullDown( if (entry.getKey().equals(defaultKey)) { doc.append( "<option value=\"" - + entry.getValue() + + escapeHtml4(entry.getValue()) + "\" selected=\"selected\">" - + entry.getKey() + + escapeHtml4(entry.getKey()) + "</option>\n"); } else { doc.append( "<option value=\"" - + entry.getValue() + + escapeHtml4(entry.getValue()) + "\">" - + entry.getKey() + + escapeHtml4(entry.getKey()) + "</option>\n"); } } @@ -631,7 +635,10 @@ private void makePullDown( private void makeFormHeader(StringBuilder doc, TileLayer tl) { doc.append("<h4>Create a new task:</h4>\n"); - doc.append("<form id=\"seed\" action=\"./" + tl.getName() + "\" method=\"post\">\n"); + doc.append( + "<form id=\"seed\" action=\"./" + + escapeHtml4(tl.getName()) + + "\" method=\"post\">\n"); doc.append("<table border=\"0\" cellspacing=\"10\">\n"); } @@ -720,9 +727,9 @@ private void makeTaskList(StringBuilder doc, TileLayer tl, boolean listAll) { doc.append("<td style=\"text-align:right\">").append(task.getTaskId()).append("</td>"); doc.append("<td>"); if (!layerName.equals(task.getLayerName())) { - doc.append("<a href=\"./").append(task.getLayerName()).append("\">"); + doc.append("<a href=\"./").append(escapeHtml4(task.getLayerName())).append("\">"); } - doc.append(task.getLayerName()); + doc.append(escapeHtml4(task.getLayerName())); if (!layerName.equals(task.getLayerName())) { doc.append("</a>"); } @@ -745,7 +752,7 @@ private void makeTaskList(StringBuilder doc, TileLayer tl, boolean listAll) { if (tasks) { doc.append("</table>"); } - doc.append("<p><a href=\"./" + layerName + "\">Refresh list</a></p>\n"); + doc.append("<p><a href=\"./" + escapeHtml4(layerName) + "\">Refresh list</a></p>\n"); } private String toTimeString(long timeSeconds, final long tilesDone, final long tilesTotal) { @@ -786,7 +793,7 @@ private String toTimeString(long timeSeconds, final long tilesDone, final long t private String makeThreadKillForm(Long key, TileLayer tl) { String ret = "<form form id=\"kill\" action=\"./" - + tl.getName() + + escapeHtml4(tl.getName()) + "\" method=\"post\">" + "<input type=\"hidden\" name=\"kill_thread\" value=\"1\" />" + "<input type=\"hidden\" name=\"thread_id\" value=\"" @@ -814,7 +821,7 @@ private String makeKillallThreadsForm(TileLayer tl, boolean listAll) { doc.append("<table><tr><td>"); doc.append("<form form id=\"list\" action=\"./") - .append(layerName) + .append(escapeHtml4(layerName)) .append("\" method=\"post\">\n"); doc.append("List "); doc.append("<select name=\"list\" onchange=\"this.form.submit();\">\n"); @@ -839,15 +846,15 @@ private String makeKillallThreadsForm(TileLayer tl, boolean listAll) { doc.append("</td></tr><tr><td>"); doc.append("<form form id=\"kill\" action=\"./") - .append(layerName) + .append(escapeHtml4(layerName)) .append("\" method=\"post\">\n"); doc.append("<span>Kill \n"); doc.append("<select name=\"kill_all\">\n"); doc.append("<option value=\"all\">all</option>\n"); doc.append("<option value=\"running\">running</option>\n"); doc.append("<option value=\"pending\">pending</option>\n"); doc.append("</select>\n"); - doc.append(" Tasks for Layer '").append(layerName).append("'."); + doc.append(" Tasks for Layer '").append(escapeHtml4(layerName)).append("'."); doc.append("<input type=\"submit\" value=\" Submit\">"); doc.append("</span>\n"); doc.append("</form>\n");
geowebcache/rest/src/test/java/org/geowebcache/rest/service/FormServiceTest.java+42 −0 modified@@ -18,20 +18,27 @@ import static org.easymock.EasyMock.expect; import static org.easymock.EasyMock.replay; import static org.easymock.EasyMock.verify; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasProperty; +import static org.hamcrest.Matchers.not; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; import org.apache.commons.collections4.iterators.EmptyIterator; import org.easymock.EasyMock; import org.geowebcache.MockWepAppContextRule; +import org.geowebcache.filter.parameters.ParameterFilter; +import org.geowebcache.filter.parameters.RegexParameterFilter; +import org.geowebcache.filter.parameters.StringParameterFilter; import org.geowebcache.grid.BoundingBox; import org.geowebcache.grid.GridSubset; import org.geowebcache.layer.TileLayer; @@ -58,6 +65,41 @@ public void setUp() throws Exception { service.setTileBreeder(breeder); } + @Test + public void testEscaping() throws Exception { + String unescapedLayer = "layer\"><"; + String escapedLayer = "layer"><"; + String unescapedString = "string\"><"; + String escapedString = "string"><"; + String unescapedRegex = "regex\"><"; + String escapedRegex = "regex"><"; + StringParameterFilter stringFilter = new StringParameterFilter(); + stringFilter.setKey(unescapedString); + RegexParameterFilter regexFilter = new RegexParameterFilter(); + regexFilter.setKey(unescapedRegex); + List<ParameterFilter> filters = Arrays.asList(stringFilter, regexFilter); + + TileLayer tl = EasyMock.createMock("tl", TileLayer.class); + expect(breeder.findTileLayer(unescapedLayer)).andReturn(tl); + expect(tl.getName()).andStubReturn(unescapedLayer); + expect(breeder.getRunningAndPendingTasks()).andReturn(Collections.emptyIterator()).times(2); + expect(tl.getGridSubsets()).andReturn(Collections.emptySet()).times(4); + expect(tl.getMimeTypes()).andReturn(Collections.emptyList()); + expect(tl.getParameterFilters()).andReturn(filters); + replay(tl, breeder); + ResponseEntity<?> response = service.handleGet(null, unescapedLayer); + verify(tl, breeder); + + assertEquals(HttpStatus.OK, response.getStatusCode()); + String body = (String) response.getBody(); + assertThat(body, not(containsString(unescapedLayer))); + assertThat(body, containsString(escapedLayer)); + assertThat(body, not(containsString(unescapedString))); + assertThat(body, containsString(escapedString)); + assertThat(body, not(containsString(unescapedRegex))); + assertThat(body, containsString(escapedRegex)); + } + @Test public void testKill() { Map<String, String> form = new HashMap<>();
9d010e09c784[GWC-1172] Improve handling special characters in the GWC Seed Form
2 files changed · +66 −17
geowebcache/rest/src/main/java/org/geowebcache/rest/service/FormService.java+24 −17 modified@@ -18,6 +18,7 @@ */ package org.geowebcache.rest.service; +import static org.apache.commons.text.StringEscapeUtils.escapeHtml4; import static org.geowebcache.seed.TileBreeder.TILE_FAILURE_RETRY_COUNT_DEFAULT; import static org.geowebcache.seed.TileBreeder.TILE_FAILURE_RETRY_WAIT_TIME_DEFAULT; import static org.geowebcache.seed.TileBreeder.TOTAL_FAILURES_BEFORE_ABORTING_DEFAULT; @@ -356,7 +357,10 @@ private void makeModifiableParameters(StringBuilder doc, TileLayer tl) { String key = pf.getKey(); String defaultValue = pf.getDefaultValue(); List<String> legalValues = pf.getLegalValues(); - doc.append("<tr><td>").append(key.toUpperCase()).append(": ").append("</td><td>"); + doc.append("<tr><td>") + .append(escapeHtml4(key.toUpperCase())) + .append(": ") + .append("</td><td>"); String parameterId = "parameter_" + key; if (pf instanceof StringParameterFilter) { Map<String, String> keysValues = makeParametersMap(defaultValue, legalValues); @@ -487,7 +491,7 @@ private void makeBboxHints(StringBuilder doc, TileLayer tl) { GridSubset subset = tl.getGridSubset(gridSetId); doc.append( "<li>" - + gridSetId + + escapeHtml4(gridSetId) + ": " + subset.getOriginalExtent().toString() + "</li>\n"); @@ -501,11 +505,11 @@ private void makeTextInput(StringBuilder doc, String id, int size) { private void makeTextInput(StringBuilder doc, String id, int size, String defaultValue) { doc.append( "<input name=\"" - + id + + escapeHtml4(id) + "\" type=\"text\" size=\"" + size + "\" value=\"" - + defaultValue + + escapeHtml4(defaultValue) + "\"/>"); } @@ -603,7 +607,7 @@ private void makeGridSetPulldown(StringBuilder doc, TileLayer tl) { private void makePullDown( StringBuilder doc, String id, Map<String, String> keysValues, String defaultKey) { - doc.append("<select name=\"" + id + "\">\n"); + doc.append("<select name=\"" + escapeHtml4(id) + "\">\n"); Iterator<Map.Entry<String, String>> iter = keysValues.entrySet().iterator(); @@ -612,16 +616,16 @@ private void makePullDown( if (entry.getKey().equals(defaultKey)) { doc.append( "<option value=\"" - + entry.getValue() + + escapeHtml4(entry.getValue()) + "\" selected=\"selected\">" - + entry.getKey() + + escapeHtml4(entry.getKey()) + "</option>\n"); } else { doc.append( "<option value=\"" - + entry.getValue() + + escapeHtml4(entry.getValue()) + "\">" - + entry.getKey() + + escapeHtml4(entry.getKey()) + "</option>\n"); } } @@ -631,7 +635,10 @@ private void makePullDown( private void makeFormHeader(StringBuilder doc, TileLayer tl) { doc.append("<h4>Create a new task:</h4>\n"); - doc.append("<form id=\"seed\" action=\"./" + tl.getName() + "\" method=\"post\">\n"); + doc.append( + "<form id=\"seed\" action=\"./" + + escapeHtml4(tl.getName()) + + "\" method=\"post\">\n"); doc.append("<table border=\"0\" cellspacing=\"10\">\n"); } @@ -720,9 +727,9 @@ private void makeTaskList(StringBuilder doc, TileLayer tl, boolean listAll) { doc.append("<td style=\"text-align:right\">").append(task.getTaskId()).append("</td>"); doc.append("<td>"); if (!layerName.equals(task.getLayerName())) { - doc.append("<a href=\"./").append(task.getLayerName()).append("\">"); + doc.append("<a href=\"./").append(escapeHtml4(task.getLayerName())).append("\">"); } - doc.append(task.getLayerName()); + doc.append(escapeHtml4(task.getLayerName())); if (!layerName.equals(task.getLayerName())) { doc.append("</a>"); } @@ -745,7 +752,7 @@ private void makeTaskList(StringBuilder doc, TileLayer tl, boolean listAll) { if (tasks) { doc.append("</table>"); } - doc.append("<p><a href=\"./" + layerName + "\">Refresh list</a></p>\n"); + doc.append("<p><a href=\"./" + escapeHtml4(layerName) + "\">Refresh list</a></p>\n"); } private String toTimeString(long timeSeconds, final long tilesDone, final long tilesTotal) { @@ -786,7 +793,7 @@ private String toTimeString(long timeSeconds, final long tilesDone, final long t private String makeThreadKillForm(Long key, TileLayer tl) { String ret = "<form form id=\"kill\" action=\"./" - + tl.getName() + + escapeHtml4(tl.getName()) + "\" method=\"post\">" + "<input type=\"hidden\" name=\"kill_thread\" value=\"1\" />" + "<input type=\"hidden\" name=\"thread_id\" value=\"" @@ -814,7 +821,7 @@ private String makeKillallThreadsForm(TileLayer tl, boolean listAll) { doc.append("<table><tr><td>"); doc.append("<form form id=\"list\" action=\"./") - .append(layerName) + .append(escapeHtml4(layerName)) .append("\" method=\"post\">\n"); doc.append("List "); doc.append("<select name=\"list\" onchange=\"this.form.submit();\">\n"); @@ -839,15 +846,15 @@ private String makeKillallThreadsForm(TileLayer tl, boolean listAll) { doc.append("</td></tr><tr><td>"); doc.append("<form form id=\"kill\" action=\"./") - .append(layerName) + .append(escapeHtml4(layerName)) .append("\" method=\"post\">\n"); doc.append("<span>Kill \n"); doc.append("<select name=\"kill_all\">\n"); doc.append("<option value=\"all\">all</option>\n"); doc.append("<option value=\"running\">running</option>\n"); doc.append("<option value=\"pending\">pending</option>\n"); doc.append("</select>\n"); - doc.append(" Tasks for Layer '").append(layerName).append("'."); + doc.append(" Tasks for Layer '").append(escapeHtml4(layerName)).append("'."); doc.append("<input type=\"submit\" value=\" Submit\">"); doc.append("</span>\n"); doc.append("</form>\n");
geowebcache/rest/src/test/java/org/geowebcache/rest/service/FormServiceTest.java+42 −0 modified@@ -18,20 +18,27 @@ import static org.easymock.EasyMock.expect; import static org.easymock.EasyMock.replay; import static org.easymock.EasyMock.verify; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasProperty; +import static org.hamcrest.Matchers.not; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; import org.apache.commons.collections4.iterators.EmptyIterator; import org.easymock.EasyMock; import org.geowebcache.MockWepAppContextRule; +import org.geowebcache.filter.parameters.ParameterFilter; +import org.geowebcache.filter.parameters.RegexParameterFilter; +import org.geowebcache.filter.parameters.StringParameterFilter; import org.geowebcache.grid.BoundingBox; import org.geowebcache.grid.GridSubset; import org.geowebcache.layer.TileLayer; @@ -58,6 +65,41 @@ public void setUp() throws Exception { service.setTileBreeder(breeder); } + @Test + public void testEscaping() throws Exception { + String unescapedLayer = "layer\"><"; + String escapedLayer = "layer"><"; + String unescapedString = "string\"><"; + String escapedString = "string"><"; + String unescapedRegex = "regex\"><"; + String escapedRegex = "regex"><"; + StringParameterFilter stringFilter = new StringParameterFilter(); + stringFilter.setKey(unescapedString); + RegexParameterFilter regexFilter = new RegexParameterFilter(); + regexFilter.setKey(unescapedRegex); + List<ParameterFilter> filters = Arrays.asList(stringFilter, regexFilter); + + TileLayer tl = EasyMock.createMock("tl", TileLayer.class); + expect(breeder.findTileLayer(unescapedLayer)).andReturn(tl); + expect(tl.getName()).andStubReturn(unescapedLayer); + expect(breeder.getRunningAndPendingTasks()).andReturn(Collections.emptyIterator()).times(2); + expect(tl.getGridSubsets()).andReturn(Collections.emptySet()).times(4); + expect(tl.getMimeTypes()).andReturn(Collections.emptyList()); + expect(tl.getParameterFilters()).andReturn(filters); + replay(tl, breeder); + ResponseEntity<?> response = service.handleGet(null, unescapedLayer); + verify(tl, breeder); + + assertEquals(HttpStatus.OK, response.getStatusCode()); + String body = (String) response.getBody(); + assertThat(body, not(containsString(unescapedLayer))); + assertThat(body, containsString(escapedLayer)); + assertThat(body, not(containsString(unescapedString))); + assertThat(body, containsString(escapedString)); + assertThat(body, not(containsString(unescapedRegex))); + assertThat(body, containsString(escapedRegex)); + } + @Test public void testKill() { Map<String, String> form = new HashMap<>();
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-56r3-f536-5gf7ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-23643ghsaADVISORY
- github.com/GeoWebCache/geowebcache/commit/9d010e09c784690ada8af43f594461a2553a62f0ghsax_refsource_MISCWEB
- github.com/GeoWebCache/geowebcache/commit/c0ca08a20bc0e66dafbdb083f7508b372c0703eeghsaWEB
- github.com/GeoWebCache/geowebcache/issues/1172ghsax_refsource_MISCWEB
- github.com/GeoWebCache/geowebcache/pull/1174ghsax_refsource_MISCWEB
- github.com/geoserver/geoserver/security/advisories/GHSA-56r3-f536-5gf7ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.