VYPR
Moderate severityNVD Advisory· Published Mar 20, 2024· Updated Aug 21, 2024

GeoServer Stored Cross-Site Scripting (XSS) vulnerability in REST Resources API

CVE-2023-51445

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.

PackageAffected versionsPatched versions
org.geoserver:gs-restconfigMaven
< 2.23.32.23.3

Affected products

1

Patches

1
7db985738ff2

[GEOS-11148] Update response headers for the Resources REST API (#7163)

https://github.com/geoserver/geoserverAndrea AimeOct 9, 2023via ghsa
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

News mentions

0

No linked articles in our index yet.