GeoServer arbitrary file renaming vulnerability in REST Coverage/Data Store API
Description
GeoServer is an open source software server written in Java that allows users to share and edit geospatial data. An arbitrary file renaming vulnerability exists in versions prior to 2.23.5 and 2.24.2 that enables an authenticated administrator with permissions to modify stores through the REST Coverage Store or Data Store API to rename arbitrary files and directories with a name that does not end in .zip. Store file uploads rename zip files to have a .zip extension if it doesn't already have one before unzipping the file. This is fine for file and url upload methods where the files will be in a specific subdirectory of the data directory but, when using the external upload method, this allows arbitrary files and directories to be renamed. Renaming GeoServer files will most likely result in a denial of service, either completely preventing GeoServer from running or effectively deleting specific resources (such as a workspace, layer or style). In some cases, renaming GeoServer files could revert to the default settings for that file which could be relatively harmless like removing contact information or have more serious consequences like allowing users to make OGC requests that the customized settings would have prevented them from making. The impact of renaming non-GeoServer files depends on the specific environment although some sort of denial of service is a likely outcome. Versions 2.23.5 and 2.24.2 contain a fix for this issue.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.geoserver:gs-restconfigMaven | < 2.23.5 | 2.23.5 |
org.geoserver:gs-restconfigMaven | >= 2.24.0, < 2.24.2 | 2.24.2 |
Affected products
1Patches
2c37f58fbacdf[GEOS-11213] Improve REST external upload method unzipping (#7359)
5 files changed · +245 −28
src/restconfig/src/main/java/org/geoserver/rest/catalog/AbstractStoreUploadController.java+29 −9 modified@@ -95,23 +95,36 @@ protected List<Resource> handleFileUpload( // handle the case that the uploaded file was a zip file, if so unzip it if (RESTUtils.isZipMediaType(request)) { // rename to .zip if need be - if (!uploadedFile.name().endsWith(".zip")) { + if (external || !uploadedFile.name().endsWith(".zip")) { + // for file and url upload methods, rename files in their current directory + // for external upload method, copy the file into a directory where it can + // be more safely unzipped Resource newUploadedFile = - uploadedFile - .parent() + (external ? directory : uploadedFile.parent()) .get(FilenameUtils.getBaseName(uploadedFile.path()) + ".zip"); String oldFileName = uploadedFile.name(); - if (!uploadedFile.renameTo(newUploadedFile)) { - String errorMessage = - "Error renaming zip file from " - + oldFileName - + " -> " - + newUploadedFile.name(); + String errorMessage = + "Error renaming zip file from " + + oldFileName + + " -> " + + newUploadedFile.name(); + // do not rename or copy directories (only possible with external upload) + // do not allow renaming/copying to overwrite an existing directory + if (uploadedFile.getType() != Resource.Type.RESOURCE + || newUploadedFile.getType() == Resource.Type.DIRECTORY + || (!external && !uploadedFile.renameTo(newUploadedFile))) { throw new RestException(errorMessage, HttpStatus.INTERNAL_SERVER_ERROR); + } else if (external) { + try { + Resources.copy(uploadedFile, newUploadedFile); + } catch (Exception e) { + throw new RestException(errorMessage, HttpStatus.INTERNAL_SERVER_ERROR, e); + } } uploadedFile = newUploadedFile; } // unzip the file + boolean success = false; try { // Unzipping of the file and, if it is a POST request, filling of the File List RESTUtils.unzipFile(uploadedFile, directory, workspace, store, files, external); @@ -121,6 +134,7 @@ protected List<Resource> handleFileUpload( Resource primaryFile = findPrimaryFile(directory, format); if (primaryFile != null) { uploadedFile = primaryFile; + success = true; } else { throw new RestException( "Could not find appropriate " + format + " file in archive", @@ -131,6 +145,12 @@ protected List<Resource> handleFileUpload( } catch (Exception e) { throw new RestException( "Error occured unzipping file", HttpStatus.INTERNAL_SERVER_ERROR, e); + } finally { + if (!success) { + // clean up files if not successful + files.forEach(Resource::delete); + uploadedFile.delete(); + } } } // If the File List is empty then the uploaded file must be added
src/restconfig/src/main/java/org/geoserver/rest/catalog/CoverageStoreFileController.java+8 −10 modified@@ -443,16 +443,14 @@ protected List<Resource> doFileUpload( boolean postRequest = request != null && HttpMethod.POST.name().equalsIgnoreCase(request.getMethod()); - // Prepare the directory only in case this is not an external upload - if (method.isInline()) { - // Mapping of the input directory - if (method == UploadMethod.url) { - // For URL upload method, workspace and StoreName are not considered - directory = RESTUtils.createUploadRoot(catalog, null, null, postRequest); - } else { - directory = - RESTUtils.createUploadRoot(catalog, workspaceName, storeName, postRequest); - } + // Mapping of the input directory + if (method == UploadMethod.url) { + // For URL upload method, workspace and StoreName are not considered + directory = RESTUtils.createUploadRoot(catalog, null, null, postRequest); + } else if (method == UploadMethod.file + || (method == UploadMethod.external && RESTUtils.isZipMediaType(request))) { + // Prepare the directory for file upload or external upload of a zip file + directory = RESTUtils.createUploadRoot(catalog, workspaceName, storeName, postRequest); } return handleFileUpload( storeName, workspaceName, filename, method, format, directory, request);
src/restconfig/src/main/java/org/geoserver/rest/catalog/DataStoreFileController.java+9 −9 modified@@ -40,6 +40,7 @@ import org.geoserver.rest.RestException; import org.geoserver.rest.util.IOUtils; import org.geoserver.rest.util.RESTUploadPathMapper; +import org.geoserver.rest.util.RESTUtils; import org.geotools.data.DataAccess; import org.geotools.data.DataAccessFactory; import org.geotools.data.DataStore; @@ -567,15 +568,14 @@ protected List<Resource> doFileUpload( boolean postRequest = request != null && HttpMethod.POST.name().equalsIgnoreCase(request.getMethod()); - // Prepare the directory only in case this is not an external upload - if (method.isInline()) { - // Mapping of the input directory - if (method == UploadMethod.url) { - // For URL upload method, workspace and StoreName are not considered - directory = createFinalRoot(null, null, postRequest); - } else { - directory = createFinalRoot(workspaceName, storeName, postRequest); - } + // Mapping of the input directory + if (method == UploadMethod.url) { + // For URL upload method, workspace and StoreName are not considered + directory = createFinalRoot(null, null, postRequest); + } else if (method == UploadMethod.file + || (method == UploadMethod.external && RESTUtils.isZipMediaType(request))) { + // Prepare the directory for file upload or external upload of a zip file + directory = createFinalRoot(workspaceName, storeName, postRequest); } return handleFileUpload( storeName, workspaceName, filename, method, format, directory, request);
src/restconfig/src/test/java/org/geoserver/rest/catalog/CoverageStoreFileUploadTest.java+102 −0 modified@@ -5,9 +5,14 @@ */ package org.geoserver.rest.catalog; +import static org.geoserver.rest.RestBaseController.ROOT_PATH; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.startsWith; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import java.io.ByteArrayInputStream; @@ -48,8 +53,10 @@ import org.geotools.util.URLs; import org.geotools.util.factory.GeoTools; import org.junit.Before; +import org.junit.ClassRule; import org.junit.Ignore; import org.junit.Test; +import org.junit.rules.TemporaryFolder; import org.opengis.coverage.grid.GridCoverageReader; import org.opengis.referencing.FactoryException; import org.springframework.http.MediaType; @@ -59,6 +66,8 @@ public class CoverageStoreFileUploadTest extends CatalogRESTTestSupport { + @ClassRule public static TemporaryFolder temp = new TemporaryFolder(); + @Override protected void onSetUp(SystemTestData testData) throws Exception { super.onSetUp(testData); @@ -83,6 +92,7 @@ public void cleanup() throws IOException { removeStore( coverage.getStore().getWorkspace().getName(), coverage.getStore().getName()); } + removeStore("sf", "usa"); } @Test @@ -754,4 +764,96 @@ static void testBBoxLayerConfiguration( assertConsumer.accept(current, old); } } + + @Test + public void testWorldImageUploadExternalZipDirectory() throws Exception { + // get the path to a directory + File file = temp.getRoot(); + String body = file.getAbsolutePath(); + // the request will fail since it won't attempt to copy a directory + MockHttpServletResponse response = + putAsServletResponse( + ROOT_PATH + "/workspaces/foo/coveragestores/bar/external.worldimage", + body, + "application/zip"); + assertEquals(500, response.getStatus()); + assertThat(response.getContentAsString(), startsWith("Error renaming zip file from ")); + // verify that the external file was not deleted + assertTrue("The external file was unexpectedly deleted", file.exists()); + } + + @Test + public void testWorldImageUploadExternalZipExistingDirectory() throws Exception { + // create a file to copy and get its path + File file1 = temp.newFile("test1.zip"); + String body = file1.getAbsolutePath(); + // create the file in the data directory + File file2 = getResourceLoader().createDirectory("data/foo/bar1/test1.zip"); + // the request will fail since it won't overwrite an existing zip file + MockHttpServletResponse response = + putAsServletResponse( + ROOT_PATH + "/workspaces/foo/coveragestores/bar1/external.worldimage", + body, + "application/zip"); + assertEquals(500, response.getStatus()); + assertThat(response.getContentAsString(), startsWith("Error renaming zip file from ")); + // verify that the external file was not deleted + assertTrue("The external file was unexpectedly deleted", file1.exists()); + // verify that the file in the data directory was not deleted + assertTrue("The file in the data directory was unexpectedly deleted", file2.isDirectory()); + } + + @Test + public void testWorldImageUploadExternalZipBadFile() throws Exception { + // create a file that is not a valid zip file and get its path + File file = temp.newFile("test2.zip"); + String body = file.getAbsolutePath(); + // the request will fail unzipping since it is not a valid zip fail + MockHttpServletResponse response = + putAsServletResponse( + ROOT_PATH + "/workspaces/foo/coveragestores/bar2/external.worldimage", + body, + "application/zip"); + assertEquals(500, response.getStatus()); + assertEquals("Error occured unzipping file", response.getContentAsString()); + // verify that the external file was not deleted + assertTrue("The external file was unexpectedly deleted", file.exists()); + // verify that the zip file was deleted from the data directory + assertEquals( + "The data directory file was not deleted", + Resource.Type.UNDEFINED, + getResourceLoader().get("data/foo/bar2/test2.zip").getType()); + } + + @Test + public void testWorldImageUploadExternalZipValid() throws Exception { + // create a valid zip file and get its path + File file = temp.newFile("test3.zip"); + FileUtils.copyURLToFile(getClass().getResource("test-data/usa.zip"), file); + String body = file.getAbsolutePath(); + // verify that the coverage does not already exist + assertNull(getCatalog().getCoverageStoreByName("sf", "usa")); + assertNull(getCatalog().getCoverageByName("sf", "usa")); + // the request should succeed + MockHttpServletResponse response = + putAsServletResponse( + ROOT_PATH + "/workspaces/sf/coveragestores/usa/external.worldimage", + body, + "application/zip"); + assertEquals(201, response.getStatus()); + assertEquals(MediaType.APPLICATION_XML_VALUE, response.getContentType()); + String content = response.getContentAsString(); + Document d = dom(new ByteArrayInputStream(content.getBytes())); + assertEquals("coverageStore", d.getDocumentElement().getNodeName()); + // verify that the coverage was created successfully + assertNotNull(getCatalog().getCoverageStoreByName("sf", "usa")); + assertNotNull(getCatalog().getCoverageByName("sf", "usa")); + // verify that the external file was not deleted + assertTrue("The external file was unexpectedly deleted", file.exists()); + // verify that the zip file was deleted from the data directory + assertEquals( + "The data directory file was not deleted", + Resource.Type.UNDEFINED, + getResourceLoader().get("data/sf/usa/test3.zip").getType()); + } }
src/restconfig/src/test/java/org/geoserver/rest/catalog/DataStoreFileUploadTest.java+97 −0 modified@@ -5,6 +5,8 @@ package org.geoserver.rest.catalog; import static org.geoserver.rest.RestBaseController.ROOT_PATH; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.startsWith; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; @@ -19,6 +21,7 @@ import java.io.OutputStreamWriter; import java.net.URL; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -34,18 +37,24 @@ import org.geoserver.data.test.SystemTestData; import org.geoserver.filters.LoggingFilter; import org.geoserver.platform.GeoServerResourceLoader; +import org.geoserver.platform.resource.Resource; import org.geotools.util.URLs; import org.h2.tools.DeleteDbFiles; import org.junit.After; import org.junit.Before; +import org.junit.ClassRule; import org.junit.Ignore; import org.junit.Test; +import org.junit.rules.TemporaryFolder; import org.springframework.mock.web.MockHttpServletResponse; import org.w3c.dom.Document; import org.w3c.dom.Node; import org.w3c.dom.NodeList; public class DataStoreFileUploadTest extends CatalogRESTTestSupport { + + @ClassRule public static TemporaryFolder temp = new TemporaryFolder(); + @Override protected void onSetUp(SystemTestData testData) throws Exception { super.onSetUp(testData); @@ -73,6 +82,7 @@ protected List<Filter> getFilters() { public void removePdsDataStore() { removeStore("gs", "pds"); removeStore("gs", "store with spaces"); + removeStore("gs", "san_andres_y_providencia"); } @After @@ -406,4 +416,91 @@ private byte[] appSchemaAlternativeMappingAsBytes() throws Exception { return null; } } + + @Test + public void testShapefileUploadExternalZipDirectory() throws Exception { + // get the path to a directory + File file = temp.getRoot(); + String body = file.getAbsolutePath(); + // the request will fail since it won't attempt to copy a directory + MockHttpServletResponse response = + putAsServletResponse( + ROOT_PATH + "/workspaces/foo/datastores/bar/external.shp", + body, + "application/zip"); + assertEquals(500, response.getStatus()); + assertThat(response.getContentAsString(), startsWith("Error renaming zip file from ")); + // verify that the external file was not deleted + assertTrue("The external file was unexpectedly deleted", file.exists()); + } + + @Test + public void testShapefileUploadExternalZipExistingDirectory() throws Exception { + // create a file to copy and get its path + File file1 = temp.newFile("test1.zip"); + String body = file1.getAbsolutePath(); + // create the file in the data directory + File file2 = getResourceLoader().createDirectory("data/foo/bar1/test1.zip"); + // the request will fail since it won't overwrite an existing zip file + MockHttpServletResponse response = + putAsServletResponse( + ROOT_PATH + "/workspaces/foo/datastores/bar1/external.shp", + body, + "application/zip"); + assertEquals(500, response.getStatus()); + assertThat(response.getContentAsString(), startsWith("Error renaming zip file from ")); + // verify that the external file was not deleted + assertTrue("The external file was unexpectedly deleted", file1.exists()); + // verify that the file in the data directory was not deleted + assertTrue("The file in the data directory was unexpectedly deleted", file2.isDirectory()); + } + + @Test + public void testShapefileUploadExternalZipBadFile() throws Exception { + // create a file that is not a valid zip file and get its path + File file = temp.newFile("test2.zip"); + String body = file.getAbsolutePath(); + // the request will fail unzipping since it is not a valid zip fail + MockHttpServletResponse response = + putAsServletResponse( + ROOT_PATH + "/workspaces/foo/datastores/bar2/external.shp", + body, + "application/zip"); + assertEquals(500, response.getStatus()); + assertEquals("Error occured unzipping file", response.getContentAsString()); + // verify that the external file was not deleted + assertTrue("The external file was unexpectedly deleted", file.exists()); + // verify that the zip file was deleted from the data directory + assertEquals( + "The data directory file was not deleted", + Resource.Type.UNDEFINED, + getResourceLoader().get("data/foo/bar2/test2.zip").getType()); + } + + @Test + public void testShapefileUploadExternalZipValid() throws Exception { + // create a valid zip file and get its path + File file = temp.newFile("test3.zip"); + Files.write(file.toPath(), shpSanAndresShapefilesZipAsBytes()); + String body = file.getAbsolutePath(); + // verify that the datastore does not already exist + Catalog cat = getCatalog(); + assertNull(cat.getDataStoreByName("gs", "san_andres_y_providencia")); + // the request should succeed + put( + ROOT_PATH + "/workspaces/gs/datastores/san_andres_y_providencia/external.shp", + body, + "application/zip"); + // verify that the datastore was created successfully + DataStoreInfo ds = cat.getDataStoreByName("gs", "san_andres_y_providencia"); + assertNotNull(ds); + assertEquals(1, cat.getFeatureTypesByDataStore(ds).size()); + // verify that the external file was not deleted + assertTrue("The external file was unexpectedly deleted", file.exists()); + // verify that the zip file was deleted from the data directory + assertEquals( + "The data directory file was not deleted", + Resource.Type.UNDEFINED, + getResourceLoader().get("data/gs/san_andres_y_providencia/test3.zip").getType()); + } }
5d6af2f8ba9a[GEOS-11213] Improve REST external upload method unzipping
5 files changed · +245 −28
src/restconfig/src/main/java/org/geoserver/rest/catalog/AbstractStoreUploadController.java+29 −9 modified@@ -95,23 +95,36 @@ protected List<Resource> handleFileUpload( // handle the case that the uploaded file was a zip file, if so unzip it if (RESTUtils.isZipMediaType(request)) { // rename to .zip if need be - if (!uploadedFile.name().endsWith(".zip")) { + if (external || !uploadedFile.name().endsWith(".zip")) { + // for file and url upload methods, rename files in their current directory + // for external upload method, copy the file into a directory where it can + // be more safely unzipped Resource newUploadedFile = - uploadedFile - .parent() + (external ? directory : uploadedFile.parent()) .get(FilenameUtils.getBaseName(uploadedFile.path()) + ".zip"); String oldFileName = uploadedFile.name(); - if (!uploadedFile.renameTo(newUploadedFile)) { - String errorMessage = - "Error renaming zip file from " - + oldFileName - + " -> " - + newUploadedFile.name(); + String errorMessage = + "Error renaming zip file from " + + oldFileName + + " -> " + + newUploadedFile.name(); + // do not rename or copy directories (only possible with external upload) + // do not allow renaming/copying to overwrite an existing directory + if (uploadedFile.getType() != Resource.Type.RESOURCE + || newUploadedFile.getType() == Resource.Type.DIRECTORY + || (!external && !uploadedFile.renameTo(newUploadedFile))) { throw new RestException(errorMessage, HttpStatus.INTERNAL_SERVER_ERROR); + } else if (external) { + try { + Resources.copy(uploadedFile, newUploadedFile); + } catch (Exception e) { + throw new RestException(errorMessage, HttpStatus.INTERNAL_SERVER_ERROR, e); + } } uploadedFile = newUploadedFile; } // unzip the file + boolean success = false; try { // Unzipping of the file and, if it is a POST request, filling of the File List RESTUtils.unzipFile(uploadedFile, directory, workspace, store, files, external); @@ -121,6 +134,7 @@ protected List<Resource> handleFileUpload( Resource primaryFile = findPrimaryFile(directory, format); if (primaryFile != null) { uploadedFile = primaryFile; + success = true; } else { throw new RestException( "Could not find appropriate " + format + " file in archive", @@ -131,6 +145,12 @@ protected List<Resource> handleFileUpload( } catch (Exception e) { throw new RestException( "Error occured unzipping file", HttpStatus.INTERNAL_SERVER_ERROR, e); + } finally { + if (!success) { + // clean up files if not successful + files.forEach(Resource::delete); + uploadedFile.delete(); + } } } // If the File List is empty then the uploaded file must be added
src/restconfig/src/main/java/org/geoserver/rest/catalog/CoverageStoreFileController.java+8 −10 modified@@ -443,16 +443,14 @@ protected List<Resource> doFileUpload( boolean postRequest = request != null && HttpMethod.POST.name().equalsIgnoreCase(request.getMethod()); - // Prepare the directory only in case this is not an external upload - if (method.isInline()) { - // Mapping of the input directory - if (method == UploadMethod.url) { - // For URL upload method, workspace and StoreName are not considered - directory = RESTUtils.createUploadRoot(catalog, null, null, postRequest); - } else { - directory = - RESTUtils.createUploadRoot(catalog, workspaceName, storeName, postRequest); - } + // Mapping of the input directory + if (method == UploadMethod.url) { + // For URL upload method, workspace and StoreName are not considered + directory = RESTUtils.createUploadRoot(catalog, null, null, postRequest); + } else if (method == UploadMethod.file + || (method == UploadMethod.external && RESTUtils.isZipMediaType(request))) { + // Prepare the directory for file upload or external upload of a zip file + directory = RESTUtils.createUploadRoot(catalog, workspaceName, storeName, postRequest); } return handleFileUpload( storeName, workspaceName, filename, method, format, directory, request);
src/restconfig/src/main/java/org/geoserver/rest/catalog/DataStoreFileController.java+9 −9 modified@@ -40,6 +40,7 @@ import org.geoserver.rest.RestException; import org.geoserver.rest.util.IOUtils; import org.geoserver.rest.util.RESTUploadPathMapper; +import org.geoserver.rest.util.RESTUtils; import org.geotools.api.data.DataAccess; import org.geotools.api.data.DataAccessFactory; import org.geotools.api.data.DataStore; @@ -567,15 +568,14 @@ protected List<Resource> doFileUpload( boolean postRequest = request != null && HttpMethod.POST.name().equalsIgnoreCase(request.getMethod()); - // Prepare the directory only in case this is not an external upload - if (method.isInline()) { - // Mapping of the input directory - if (method == UploadMethod.url) { - // For URL upload method, workspace and StoreName are not considered - directory = createFinalRoot(null, null, postRequest); - } else { - directory = createFinalRoot(workspaceName, storeName, postRequest); - } + // Mapping of the input directory + if (method == UploadMethod.url) { + // For URL upload method, workspace and StoreName are not considered + directory = createFinalRoot(null, null, postRequest); + } else if (method == UploadMethod.file + || (method == UploadMethod.external && RESTUtils.isZipMediaType(request))) { + // Prepare the directory for file upload or external upload of a zip file + directory = createFinalRoot(workspaceName, storeName, postRequest); } return handleFileUpload( storeName, workspaceName, filename, method, format, directory, request);
src/restconfig/src/test/java/org/geoserver/rest/catalog/CoverageStoreFileUploadTest.java+102 −0 modified@@ -5,9 +5,14 @@ */ package org.geoserver.rest.catalog; +import static org.geoserver.rest.RestBaseController.ROOT_PATH; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.startsWith; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import java.io.ByteArrayInputStream; @@ -50,15 +55,19 @@ import org.geotools.util.URLs; import org.geotools.util.factory.GeoTools; import org.junit.Before; +import org.junit.ClassRule; import org.junit.Ignore; import org.junit.Test; +import org.junit.rules.TemporaryFolder; import org.springframework.http.MediaType; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.w3c.dom.Document; public class CoverageStoreFileUploadTest extends CatalogRESTTestSupport { + @ClassRule public static TemporaryFolder temp = new TemporaryFolder(); + @Override protected void onSetUp(SystemTestData testData) throws Exception { super.onSetUp(testData); @@ -83,6 +92,7 @@ public void cleanup() throws IOException { removeStore( coverage.getStore().getWorkspace().getName(), coverage.getStore().getName()); } + removeStore("sf", "usa"); } @Test @@ -754,4 +764,96 @@ static void testBBoxLayerConfiguration( assertConsumer.accept(current, old); } } + + @Test + public void testWorldImageUploadExternalZipDirectory() throws Exception { + // get the path to a directory + File file = temp.getRoot(); + String body = file.getAbsolutePath(); + // the request will fail since it won't attempt to copy a directory + MockHttpServletResponse response = + putAsServletResponse( + ROOT_PATH + "/workspaces/foo/coveragestores/bar/external.worldimage", + body, + "application/zip"); + assertEquals(500, response.getStatus()); + assertThat(response.getContentAsString(), startsWith("Error renaming zip file from ")); + // verify that the external file was not deleted + assertTrue("The external file was unexpectedly deleted", file.exists()); + } + + @Test + public void testWorldImageUploadExternalZipExistingDirectory() throws Exception { + // create a file to copy and get its path + File file1 = temp.newFile("test1.zip"); + String body = file1.getAbsolutePath(); + // create the file in the data directory + File file2 = getResourceLoader().createDirectory("data/foo/bar1/test1.zip"); + // the request will fail since it won't overwrite an existing zip file + MockHttpServletResponse response = + putAsServletResponse( + ROOT_PATH + "/workspaces/foo/coveragestores/bar1/external.worldimage", + body, + "application/zip"); + assertEquals(500, response.getStatus()); + assertThat(response.getContentAsString(), startsWith("Error renaming zip file from ")); + // verify that the external file was not deleted + assertTrue("The external file was unexpectedly deleted", file1.exists()); + // verify that the file in the data directory was not deleted + assertTrue("The file in the data directory was unexpectedly deleted", file2.isDirectory()); + } + + @Test + public void testWorldImageUploadExternalZipBadFile() throws Exception { + // create a file that is not a valid zip file and get its path + File file = temp.newFile("test2.zip"); + String body = file.getAbsolutePath(); + // the request will fail unzipping since it is not a valid zip fail + MockHttpServletResponse response = + putAsServletResponse( + ROOT_PATH + "/workspaces/foo/coveragestores/bar2/external.worldimage", + body, + "application/zip"); + assertEquals(500, response.getStatus()); + assertEquals("Error occured unzipping file", response.getContentAsString()); + // verify that the external file was not deleted + assertTrue("The external file was unexpectedly deleted", file.exists()); + // verify that the zip file was deleted from the data directory + assertEquals( + "The data directory file was not deleted", + Resource.Type.UNDEFINED, + getResourceLoader().get("data/foo/bar2/test2.zip").getType()); + } + + @Test + public void testWorldImageUploadExternalZipValid() throws Exception { + // create a valid zip file and get its path + File file = temp.newFile("test3.zip"); + FileUtils.copyURLToFile(getClass().getResource("test-data/usa.zip"), file); + String body = file.getAbsolutePath(); + // verify that the coverage does not already exist + assertNull(getCatalog().getCoverageStoreByName("sf", "usa")); + assertNull(getCatalog().getCoverageByName("sf", "usa")); + // the request should succeed + MockHttpServletResponse response = + putAsServletResponse( + ROOT_PATH + "/workspaces/sf/coveragestores/usa/external.worldimage", + body, + "application/zip"); + assertEquals(201, response.getStatus()); + assertEquals(MediaType.APPLICATION_XML_VALUE, response.getContentType()); + String content = response.getContentAsString(); + Document d = dom(new ByteArrayInputStream(content.getBytes())); + assertEquals("coverageStore", d.getDocumentElement().getNodeName()); + // verify that the coverage was created successfully + assertNotNull(getCatalog().getCoverageStoreByName("sf", "usa")); + assertNotNull(getCatalog().getCoverageByName("sf", "usa")); + // verify that the external file was not deleted + assertTrue("The external file was unexpectedly deleted", file.exists()); + // verify that the zip file was deleted from the data directory + assertEquals( + "The data directory file was not deleted", + Resource.Type.UNDEFINED, + getResourceLoader().get("data/sf/usa/test3.zip").getType()); + } }
src/restconfig/src/test/java/org/geoserver/rest/catalog/DataStoreFileUploadTest.java+97 −0 modified@@ -5,6 +5,8 @@ package org.geoserver.rest.catalog; import static org.geoserver.rest.RestBaseController.ROOT_PATH; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.startsWith; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; @@ -19,6 +21,7 @@ import java.io.OutputStreamWriter; import java.net.URL; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -34,18 +37,24 @@ import org.geoserver.data.test.SystemTestData; import org.geoserver.filters.LoggingFilter; import org.geoserver.platform.GeoServerResourceLoader; +import org.geoserver.platform.resource.Resource; import org.geotools.util.URLs; import org.h2.tools.DeleteDbFiles; import org.junit.After; import org.junit.Before; +import org.junit.ClassRule; import org.junit.Ignore; import org.junit.Test; +import org.junit.rules.TemporaryFolder; import org.springframework.mock.web.MockHttpServletResponse; import org.w3c.dom.Document; import org.w3c.dom.Node; import org.w3c.dom.NodeList; public class DataStoreFileUploadTest extends CatalogRESTTestSupport { + + @ClassRule public static TemporaryFolder temp = new TemporaryFolder(); + @Override protected void onSetUp(SystemTestData testData) throws Exception { super.onSetUp(testData); @@ -73,6 +82,7 @@ protected List<Filter> getFilters() { public void removePdsDataStore() { removeStore("gs", "pds"); removeStore("gs", "store with spaces"); + removeStore("gs", "san_andres_y_providencia"); } @After @@ -406,4 +416,91 @@ private byte[] appSchemaAlternativeMappingAsBytes() throws Exception { return null; } } + + @Test + public void testShapefileUploadExternalZipDirectory() throws Exception { + // get the path to a directory + File file = temp.getRoot(); + String body = file.getAbsolutePath(); + // the request will fail since it won't attempt to copy a directory + MockHttpServletResponse response = + putAsServletResponse( + ROOT_PATH + "/workspaces/foo/datastores/bar/external.shp", + body, + "application/zip"); + assertEquals(500, response.getStatus()); + assertThat(response.getContentAsString(), startsWith("Error renaming zip file from ")); + // verify that the external file was not deleted + assertTrue("The external file was unexpectedly deleted", file.exists()); + } + + @Test + public void testShapefileUploadExternalZipExistingDirectory() throws Exception { + // create a file to copy and get its path + File file1 = temp.newFile("test1.zip"); + String body = file1.getAbsolutePath(); + // create the file in the data directory + File file2 = getResourceLoader().createDirectory("data/foo/bar1/test1.zip"); + // the request will fail since it won't overwrite an existing zip file + MockHttpServletResponse response = + putAsServletResponse( + ROOT_PATH + "/workspaces/foo/datastores/bar1/external.shp", + body, + "application/zip"); + assertEquals(500, response.getStatus()); + assertThat(response.getContentAsString(), startsWith("Error renaming zip file from ")); + // verify that the external file was not deleted + assertTrue("The external file was unexpectedly deleted", file1.exists()); + // verify that the file in the data directory was not deleted + assertTrue("The file in the data directory was unexpectedly deleted", file2.isDirectory()); + } + + @Test + public void testShapefileUploadExternalZipBadFile() throws Exception { + // create a file that is not a valid zip file and get its path + File file = temp.newFile("test2.zip"); + String body = file.getAbsolutePath(); + // the request will fail unzipping since it is not a valid zip fail + MockHttpServletResponse response = + putAsServletResponse( + ROOT_PATH + "/workspaces/foo/datastores/bar2/external.shp", + body, + "application/zip"); + assertEquals(500, response.getStatus()); + assertEquals("Error occured unzipping file", response.getContentAsString()); + // verify that the external file was not deleted + assertTrue("The external file was unexpectedly deleted", file.exists()); + // verify that the zip file was deleted from the data directory + assertEquals( + "The data directory file was not deleted", + Resource.Type.UNDEFINED, + getResourceLoader().get("data/foo/bar2/test2.zip").getType()); + } + + @Test + public void testShapefileUploadExternalZipValid() throws Exception { + // create a valid zip file and get its path + File file = temp.newFile("test3.zip"); + Files.write(file.toPath(), shpSanAndresShapefilesZipAsBytes()); + String body = file.getAbsolutePath(); + // verify that the datastore does not already exist + Catalog cat = getCatalog(); + assertNull(cat.getDataStoreByName("gs", "san_andres_y_providencia")); + // the request should succeed + put( + ROOT_PATH + "/workspaces/gs/datastores/san_andres_y_providencia/external.shp", + body, + "application/zip"); + // verify that the datastore was created successfully + DataStoreInfo ds = cat.getDataStoreByName("gs", "san_andres_y_providencia"); + assertNotNull(ds); + assertEquals(1, cat.getFeatureTypesByDataStore(ds).size()); + // verify that the external file was not deleted + assertTrue("The external file was unexpectedly deleted", file.exists()); + // verify that the zip file was deleted from the data directory + assertEquals( + "The data directory file was not deleted", + Resource.Type.UNDEFINED, + getResourceLoader().get("data/gs/san_andres_y_providencia/test3.zip").getType()); + } }
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-75m5-hh4r-q9gxghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-23634ghsaADVISORY
- github.com/geoserver/geoserver/commit/5d6af2f8ba9ad7dffae59575504a867159698772ghsax_refsource_MISCWEB
- github.com/geoserver/geoserver/commit/c37f58fbacdfa0d581a6f99195585f70b1201f0aghsax_refsource_MISCWEB
- github.com/geoserver/geoserver/pull/7289ghsax_refsource_MISCWEB
- github.com/geoserver/geoserver/security/advisories/GHSA-75m5-hh4r-q9gxghsax_refsource_CONFIRMWEB
- osgeo-org.atlassian.net/browse/GEOS-11213ghsax_refsource_MISCWEB
News mentions
0No linked articles in our index yet.