GeoServer Stored Cross-Site Scripting (XSS) vulnerability in REST Resources API
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.0 that enables an authenticated administrator with workspace-level privileges to store a JavaScript payload in uploaded style/legend resources that will execute in the context of another administrator's browser when viewed in the REST Resources API. Access to the REST Resources API is limited to full administrators by default and granting non-administrators access to this endpoint should be carefully considered as it may allow access to files containing sensitive information. Versions 2.23.3 and 2.24.0 contain a patch for this issue.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.geoserver:gs-restconfigMaven | < 2.23.3 | 2.23.3 |
Affected products
1Patches
17db985738ff2[GEOS-11148] Update response headers for the Resources REST API (#7163)
2 files changed · +41 −30
src/restconfig/src/main/java/org/geoserver/rest/resources/ResourceController.java+23 −23 modified@@ -9,11 +9,8 @@ import com.thoughtworks.xstream.XStream; import com.thoughtworks.xstream.annotations.XStreamAlias; import freemarker.template.ObjectWrapper; -import java.io.BufferedInputStream; import java.io.IOException; -import java.io.InputStream; import java.io.UnsupportedEncodingException; -import java.net.URLConnection; import java.net.URLDecoder; import java.text.DateFormat; import java.text.SimpleDateFormat; @@ -23,6 +20,7 @@ import java.util.Date; import java.util.List; import java.util.Locale; +import java.util.Optional; import java.util.TimeZone; import java.util.logging.Level; import java.util.logging.Logger; @@ -53,6 +51,7 @@ import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.ContentDisposition; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -129,24 +128,18 @@ protected String getTemplateName(Object object) { * @return Content type requested */ protected static MediaType getMediaType(Resource resource, HttpServletRequest request) { - if (resource.getType() == Resource.Type.DIRECTORY) { - return getFormat(request); - } else if (resource.getType() == Resource.Type.RESOURCE) { - String mimeType = URLConnection.guessContentTypeFromName(resource.name()); - if (mimeType == null - || MediaType.APPLICATION_OCTET_STREAM.toString().equals(mimeType)) { - // try guessing from data - try (InputStream is = new BufferedInputStream(resource.in())) { - mimeType = URLConnection.guessContentTypeFromStream(is); - } catch (IOException e) { - // do nothing, we'll just use application/octet-stream - } - } - return mimeType == null - ? MediaType.APPLICATION_OCTET_STREAM - : MediaType.valueOf(mimeType); - } else { - return null; + switch (resource.getType()) { + case DIRECTORY: + return getFormat(request); + case RESOURCE: + // set the mime if known by the servlet container, otherwise default to + // application/octet-stream to mitigate potential cross-site scripting + return Optional.ofNullable(request.getServletContext()) + .map(sc -> sc.getMimeType(resource.name())) + .map(MediaType::valueOf) + .orElse(MediaType.APPLICATION_OCTET_STREAM); + default: + throw new ResourceNotFoundException("Undefined resource path."); } } @@ -265,21 +258,27 @@ public Object resourceGet( Resource resource = resource(request); Operation operation = operation(operationName); Object result; - response.setContentType(getFormat(format).toString()); if (operation == Operation.METADATA) { result = wrapObject( new ResourceMetadataInfo(resource, request), ResourceMetadataInfo.class); + response.setContentType(getFormat(format).toString()); } else { if (resource.getType() == Resource.Type.UNDEFINED) { throw new ResourceNotFoundException("Undefined resource path."); } else { HttpHeaders responseHeaders = new HttpHeaders(); MediaType mediaType = getMediaType(resource, request); responseHeaders.setContentType(mediaType); - response.setContentType(mediaType.toString()); + if (resource.getType() == Resource.Type.RESOURCE) { + // Use Content-Disposition: attachment to mitigate potential XSS issues + responseHeaders.setContentDisposition( + ContentDisposition.builder("attachment") + .filename(resource.name()) + .build()); + } if (request.getMethod().equals("HEAD")) { result = new ResponseEntity<>("", responseHeaders, HttpStatus.OK); @@ -288,6 +287,7 @@ public Object resourceGet( wrapObject( new ResourceDirectoryInfo(resource, request), ResourceDirectoryInfo.class); + response.setContentType(mediaType.toString()); } else { result = new ResponseEntity<>(resource.in(), responseHeaders, HttpStatus.OK); }
src/restconfig/src/test/java/org/geoserver/rest/resources/ResourceControllerTest.java+18 −7 modified@@ -5,13 +5,13 @@ package org.geoserver.rest.resources; import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import java.io.IOException; import java.io.InputStream; import java.io.OutputStreamWriter; -import java.net.URLConnection; import java.nio.charset.Charset; import java.nio.charset.CharsetEncoder; import java.text.DateFormat; @@ -239,6 +239,11 @@ public void testResourceMetadataHTML() throws Exception { public void testResourceHeaders() throws Exception { MockHttpServletResponse response = getAsServletResponse(RestBaseController.ROOT_PATH + "/resource/mydir2/fake.png"); + assertEquals( + "http://localhost:8080/geoserver" + + RestBaseController.ROOT_PATH + + "/resource/mydir2/fake.png", + response.getHeader("Location")); Assert.assertEquals( FORMAT_HEADER.format(getDataDirectory().get("mydir2/fake.png").lastmodified()), response.getHeader("Last-Modified")); @@ -249,12 +254,19 @@ public void testResourceHeaders() throws Exception { response.getHeader("Resource-Parent")); Assert.assertEquals("resource", response.getHeader("Resource-Type")); assertContentType("image/png", response); + assertEquals( + "attachment; filename=\"fake.png\"", response.getHeader("Content-Disposition")); } @Test public void testResourceHead() throws Exception { MockHttpServletResponse response = headAsServletResponse(RestBaseController.ROOT_PATH + "/resource/mydir2/fake.png"); + assertEquals( + "http://localhost:8080/geoserver" + + RestBaseController.ROOT_PATH + + "/resource/mydir2/fake.png", + response.getHeader("Location")); Assert.assertEquals( FORMAT_HEADER.format(getDataDirectory().get("mydir2/fake.png").lastmodified()), response.getHeader("Last-Modified")); @@ -265,6 +277,8 @@ public void testResourceHead() throws Exception { response.getHeader("Resource-Parent")); Assert.assertEquals("resource", response.getHeader("Resource-Type")); assertContentType("image/png", response); + assertEquals( + "attachment; filename=\"fake.png\"", response.getHeader("Content-Disposition")); } @Test @@ -409,15 +423,15 @@ public void testDirectoryJSONMultipleChildren() throws Exception { + " 'link': {\n" + " 'href': 'http://localhost:8080/geoserver/rest/resource/mydir2/imagewithoutextension',\n" + " 'rel': 'alternate',\n" - + " 'type': 'image/png'\n" + + " 'type': 'application/octet-stream'\n" + " }\n" + " },\n" + " {\n" + " 'name': 'myres.json',\n" + " 'link': {\n" + " 'href': 'http://localhost:8080/geoserver/rest/resource/mydir2/myres.json',\n" + " 'rel': 'alternate',\n" - + " 'type': 'application/octet-stream'\n" + + " 'type': 'application/json'\n" + " }\n" + " },\n" + " {\n" @@ -430,9 +444,6 @@ public void testDirectoryJSONMultipleChildren() throws Exception { + " }\n" + " ]}\n" + "}}"; - // starting with JDK 17 (v3?) json is correctly recognized, the test output - String jsonType = URLConnection.guessContentTypeFromName("test.json"); - if (jsonType != null) expected = expected.replace("application/octet-stream", jsonType); JSONAssert.assertEquals(expected, (JSONObject) json); } @@ -508,7 +519,7 @@ public void testDirectoryMimeTypes() throws Exception { Document doc = getAsDOM(RestBaseController.ROOT_PATH + "/resource/mydir2?format=xml"); // print(doc); XMLAssert.assertXpathEvaluatesTo( - "image/png", + "application/octet-stream", "/ResourceDirectory/children/child[name='imagewithoutextension']/atom:link/@type", doc); XMLAssert.assertXpathEvaluatesTo(
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
6- github.com/advisories/GHSA-fh7p-5f6g-vj2wghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2023-51445ghsaADVISORY
- github.com/geoserver/geoserver/commit/7db985738ff2422019ccac974cf547bae5770cadghsax_refsource_MISCWEB
- github.com/geoserver/geoserver/pull/7161ghsax_refsource_MISCWEB
- github.com/geoserver/geoserver/security/advisories/GHSA-fh7p-5f6g-vj2wghsax_refsource_CONFIRMWEB
- osgeo-org.atlassian.net/browse/GEOS-11148ghsax_refsource_MISCWEB
News mentions
0No linked articles in our index yet.