FINERACT-1794: Address improvement in file upload APIs
diff --git a/buildSrc/src/main/groovy/org.apache.fineract.dependencies.gradle b/buildSrc/src/main/groovy/org.apache.fineract.dependencies.gradle
index cc01b28..2f5a713 100644
--- a/buildSrc/src/main/groovy/org.apache.fineract.dependencies.gradle
+++ b/buildSrc/src/main/groovy/org.apache.fineract.dependencies.gradle
@@ -66,6 +66,38 @@
         dependency 'com.github.spullara.mustache.java:compiler:0.9.10'
         dependency 'com.jayway.jsonpath:json-path:2.7.0'
         dependency 'org.apache.tika:tika-core:2.4.0'
+        dependency ('org.apache.tika:tika-parser-microsoft-module:2.4.0') {
+            exclude 'org.bouncycastle:bcprov-jdk15on'
+            exclude 'org.bouncycastle:bcmail-jdk15on'
+            exclude 'commons-logging:commons-logging'
+            exclude 'org.apache.logging.log4j:log4j-api'
+            exclude 'org.slf4j:slf4j-api'
+            exclude 'commons-io:commons-io'
+            exclude 'commons-codec:commons-codec'
+            exclude 'org.apache.commons:commons-compress'
+            exclude 'org.apache.commons:commons-lang3'
+            exclude 'org.apache.poi:poi'
+            exclude 'org.apache.poi:poi-scratchpad'
+            exclude 'org.glassfish.jaxb:jaxb-runtime'
+            exclude 'org.apache.commons:commons-compress'
+            exclude 'xml-apis:xml-apis'
+        }
+        dependency ('org.apache.tika:tika-parser-miscoffice-module:2.4.0') {
+            exclude 'org.bouncycastle:bcprov-jdk15on'
+            exclude 'org.bouncycastle:bcmail-jdk15on'
+            exclude 'commons-logging:commons-logging'
+            exclude 'org.apache.logging.log4j:log4j-api'
+            exclude 'org.slf4j:slf4j-api'
+            exclude 'commons-io:commons-io'
+            exclude 'commons-codec:commons-codec'
+            exclude 'org.apache.commons:commons-compress'
+            exclude 'org.apache.commons:commons-lang3'
+            exclude 'org.apache.poi:poi'
+            exclude 'org.apache.poi:poi-scratchpad'
+            exclude 'org.glassfish.jaxb:jaxb-runtime'
+            exclude 'org.apache.commons:commons-compress'
+            exclude 'xml-apis:xml-apis'
+        }
         dependency 'org.apache.httpcomponents:httpclient:4.5.13'
         dependency 'jakarta.management.j2ee:jakarta.management.j2ee-api:1.1.4'
         dependency 'jakarta.jms:jakarta.jms-api:2.0.3'
diff --git a/fineract-provider/dependencies.gradle b/fineract-provider/dependencies.gradle
index d7c7fc6..0607967 100644
--- a/fineract-provider/dependencies.gradle
+++ b/fineract-provider/dependencies.gradle
@@ -52,6 +52,8 @@
             'org.apache.poi:poi',
             'org.apache.poi:poi-ooxml',
             'org.apache.tika:tika-core',
+            'org.apache.tika:tika-parser-microsoft-module',
+            'org.apache.tika:tika-parser-miscoffice-module',
 
             'org.liquibase:liquibase-core',
 
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/service/BulkImportWorkbookServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/service/BulkImportWorkbookServiceImpl.java
index c41c655..8043125 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/service/BulkImportWorkbookServiceImpl.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/service/BulkImportWorkbookServiceImpl.java
@@ -18,6 +18,7 @@
  */
 package org.apache.fineract.infrastructure.bulkimport.service;
 
+import java.io.BufferedInputStream;
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.File;
@@ -92,7 +93,7 @@
                 IOUtils.copy(inputStream, baos);
                 final byte[] bytes = baos.toByteArray();
                 InputStream clonedInputStream = new ByteArrayInputStream(bytes);
-                InputStream clonedInputStreamWorkbook = new ByteArrayInputStream(bytes);
+                final BufferedInputStream bis = new BufferedInputStream(new ByteArrayInputStream(bytes));
                 final Tika tika = new Tika();
                 final TikaInputStream tikaInputStream = TikaInputStream.get(clonedInputStream);
                 final String fileType = tika.detect(tikaInputStream);
@@ -104,7 +105,7 @@
                             "Uploaded file extension is not recognized.");
 
                 }
-                Workbook workbook = new HSSFWorkbook(clonedInputStreamWorkbook);
+                Workbook workbook = new HSSFWorkbook(clonedInputStream);
                 GlobalEntityType entityType = null;
                 int primaryColumn = 0;
                 if (entity.trim().equalsIgnoreCase(GlobalEntityType.CLIENTS_PERSON.toString())) {
@@ -169,7 +170,7 @@
                     throw new GeneralPlatformDomainRuleException("error.msg.unable.to.find.resource", "Unable to find requested resource");
 
                 }
-                return publishEvent(primaryColumn, fileDetail, clonedInputStreamWorkbook, entityType, workbook, locale, dateFormat);
+                return publishEvent(primaryColumn, fileDetail, bis, entityType, workbook, locale, dateFormat);
             }
             throw new GeneralPlatformDomainRuleException("error.msg.null", "One or more of the given parameters not found");
         } catch (IOException e) {
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/documentmanagement/contentrepository/ContentPathSanitizer.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/documentmanagement/contentrepository/ContentPathSanitizer.java
new file mode 100644
index 0000000..415532f
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/documentmanagement/contentrepository/ContentPathSanitizer.java
@@ -0,0 +1,28 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.infrastructure.documentmanagement.contentrepository;
+
+import java.io.BufferedInputStream;
+
+public interface ContentPathSanitizer {
+
+    String sanitize(String path);
+
+    String sanitize(String path, BufferedInputStream is);
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/documentmanagement/contentrepository/ContentRepositoryFactory.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/documentmanagement/contentrepository/ContentRepositoryFactory.java
index 4158fe0..1f5faab 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/documentmanagement/contentrepository/ContentRepositoryFactory.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/documentmanagement/contentrepository/ContentRepositoryFactory.java
@@ -18,26 +18,22 @@
  */
 package org.apache.fineract.infrastructure.documentmanagement.contentrepository;
 
+import lombok.RequiredArgsConstructor;
 import org.apache.fineract.infrastructure.configuration.data.S3CredentialsData;
 import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
 import org.apache.fineract.infrastructure.configuration.service.ExternalServicesPropertiesReadPlatformService;
 import org.apache.fineract.infrastructure.documentmanagement.domain.StorageType;
-import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.ApplicationContext;
 import org.springframework.stereotype.Component;
 
 @Component
+@RequiredArgsConstructor
 public class ContentRepositoryFactory {
 
     private final ApplicationContext applicationContext;
     private final ExternalServicesPropertiesReadPlatformService externalServicesReadPlatformService;
 
-    @Autowired
-    public ContentRepositoryFactory(final ApplicationContext applicationContext,
-            final ExternalServicesPropertiesReadPlatformService externalServicesReadPlatformService) {
-        this.applicationContext = applicationContext;
-        this.externalServicesReadPlatformService = externalServicesReadPlatformService;
-    }
+    private final FileSystemContentPathSanitizer contentPathSanitizer;
 
     public ContentRepository getRepository() {
         final ConfigurationDomainService configurationDomainServiceJpa = this.applicationContext.getBean("configurationDomainServiceJpa",
@@ -45,12 +41,12 @@
         if (configurationDomainServiceJpa.isAmazonS3Enabled()) {
             return createS3DocumentStore();
         }
-        return new FileSystemContentRepository();
+        return new FileSystemContentRepository(contentPathSanitizer);
     }
 
     public ContentRepository getRepository(final StorageType documentStoreType) {
         if (documentStoreType == StorageType.FILE_SYSTEM) {
-            return new FileSystemContentRepository();
+            return new FileSystemContentRepository(contentPathSanitizer);
         }
         return createS3DocumentStore();
     }
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/documentmanagement/contentrepository/FileSystemContentPathSanitizer.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/documentmanagement/contentrepository/FileSystemContentPathSanitizer.java
new file mode 100644
index 0000000..f9ce7a3
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/documentmanagement/contentrepository/FileSystemContentPathSanitizer.java
@@ -0,0 +1,143 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.infrastructure.documentmanagement.contentrepository;
+
+import java.io.BufferedInputStream;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.regex.Pattern;
+import javax.annotation.PostConstruct;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.io.FilenameUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
+import org.apache.fineract.infrastructure.documentmanagement.exception.ContentManagementException;
+import org.apache.tika.Tika;
+import org.apache.tika.io.TikaInputStream;
+import org.apache.tika.metadata.Metadata;
+import org.apache.tika.parser.AutoDetectParser;
+import org.apache.tika.sax.BodyContentHandler;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+@Slf4j
+@RequiredArgsConstructor
+@Component
+public class FileSystemContentPathSanitizer implements ContentPathSanitizer {
+
+    private static Pattern OVERWRITE_SIBLING_IMAGE = Pattern.compile(".*\\.\\./+[0-9]+/+.*");
+
+    @Value("${fineract.content.regex-whitelist-enabled}")
+    private boolean isRegexWhitelistEnabled;
+    @Value("${fineract.content.regex-whitelist}")
+    private List<String> regexWhitelist;
+    @Value("${fineract.content.mime-whitelist-enabled}")
+    private boolean isMimeWhitelistEnabled;
+    @Value("${fineract.content.mime-whitelist}")
+    private List<String> mimeWhitelist;
+    private List<Pattern> regexWhitelistPatterns;
+
+    @PostConstruct
+    public void init() {
+        regexWhitelistPatterns = regexWhitelist.stream().map(Pattern::compile).toList();
+    }
+
+    @Override
+    public String sanitize(String path) {
+        return sanitize(path, null);
+    }
+
+    @Override
+    public String sanitize(String path, BufferedInputStream is) {
+        try {
+            if (OVERWRITE_SIBLING_IMAGE.matcher(path).matches()) {
+                throw new RuntimeException(String.format("Trying to overwrite another resource's image: %s", path));
+            }
+
+            String sanitizedPath = Path.of(path).normalize().toString();
+
+            String fileName = FilenameUtils.getName(sanitizedPath).toLowerCase();
+
+            if (log.isDebugEnabled()) {
+                log.debug("Path: {} -> {} ({})", path, sanitizedPath, fileName);
+            }
+
+            if (isRegexWhitelistEnabled) {
+                boolean matches = regexWhitelistPatterns.stream().anyMatch(p -> p.matcher(fileName).matches());
+
+                if (!matches) {
+                    throw new RuntimeException(String.format("File name not allowed: %s", fileName));
+                }
+            }
+
+            if (is != null && isMimeWhitelistEnabled) {
+                Tika tika = new Tika();
+                String extensionMimeType = tika.detect(fileName);
+
+                if (StringUtils.isEmpty(extensionMimeType)) {
+                    throw new RuntimeException(String.format("Could not detect mime type for filename %s!", fileName));
+                }
+
+                if (!mimeWhitelist.contains(extensionMimeType)) {
+                    throw new RuntimeException(
+                            String.format("Detected mime type %s for filename %s not allowed!", extensionMimeType, fileName));
+                }
+
+                String contentMimeType = detectContentMimeType(is);
+
+                if (StringUtils.isEmpty(contentMimeType)) {
+                    throw new RuntimeException(String.format("Could not detect content mime type for %s!", fileName));
+                }
+
+                if (!mimeWhitelist.contains(contentMimeType)) {
+                    throw new RuntimeException(
+                            String.format("Detected content mime type %s for %s not allowed!", contentMimeType, fileName));
+                }
+
+                if (!contentMimeType.equalsIgnoreCase(extensionMimeType)) {
+                    throw new RuntimeException(String.format("Detected filename (%s) and content (%s) mime type do not match!",
+                            extensionMimeType, contentMimeType));
+                }
+            }
+
+            Path target = Path.of(sanitizedPath);
+            Path rootFolder = Path.of(FileSystemContentRepository.FINERACT_BASE_DIR,
+                    ThreadLocalContextUtil.getTenant().getName().replaceAll(" ", "").trim());
+
+            if (!target.startsWith(rootFolder)) {
+                throw new RuntimeException(String.format("Path traversal attempt: %s (%s)", target, rootFolder));
+            }
+
+            return sanitizedPath;
+        } catch (Exception e) {
+            throw new ContentManagementException(path, e.getMessage(), e);
+        }
+    }
+
+    private String detectContentMimeType(BufferedInputStream bis) throws Exception {
+        TikaInputStream tis = TikaInputStream.get(bis);
+        AutoDetectParser parser = new AutoDetectParser();
+        BodyContentHandler handler = new BodyContentHandler(-1);
+        Metadata metadata = new Metadata();
+        parser.parse(tis, handler, metadata);
+
+        return metadata.get("Content-Type");
+    }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/documentmanagement/contentrepository/FileSystemContentRepository.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/documentmanagement/contentrepository/FileSystemContentRepository.java
index 19a83e1..64e32b7 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/documentmanagement/contentrepository/FileSystemContentRepository.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/documentmanagement/contentrepository/FileSystemContentRepository.java
@@ -19,6 +19,7 @@
 package org.apache.fineract.infrastructure.documentmanagement.contentrepository;
 
 import com.google.common.io.Files;
+import java.io.BufferedInputStream;
 import java.io.ByteArrayInputStream;
 import java.io.File;
 import java.io.IOException;
@@ -42,6 +43,12 @@
 
     public static final String FINERACT_BASE_DIR = System.getProperty("user.home") + File.separator + ".fineract";
 
+    private final FileSystemContentPathSanitizer pathSanitizer;
+
+    public FileSystemContentRepository(final FileSystemContentPathSanitizer pathSanitizer) {
+        this.pathSanitizer = pathSanitizer;
+    }
+
     @Override
     public String saveFile(final InputStream uploadedInputStream, final DocumentCommand documentCommand) {
         final String fileName = documentCommand.getFileName();
@@ -88,23 +95,29 @@
     }
 
     private void deleteFileInternal(final String documentPath) {
-        final File fileToBeDeleted = new File(documentPath);
+        String path = pathSanitizer.sanitize(documentPath);
+
+        final File fileToBeDeleted = new File(path);
         final boolean fileDeleted = fileToBeDeleted.delete();
         if (!fileDeleted) {
             // no need to throw an Error, what's a caller going to do about it, so simply log a warning
-            LOG.warn("Unable to delete file {}", documentPath);
+            LOG.warn("Unable to delete file {}", path);
         }
     }
 
     @Override
     public FileData fetchFile(final DocumentData documentData) {
-        final File file = new File(documentData.fileLocation());
-        return new FileData(Files.asByteSource(file), documentData.fileName(), documentData.contentType());
+        String path = pathSanitizer.sanitize(documentData.fileLocation());
+
+        final File file = new File(path);
+        return new FileData(Files.asByteSource(file), file.getName(), documentData.contentType());
     }
 
     @Override
     public FileData fetchImage(final ImageData imageData) {
-        final File file = new File(imageData.location());
+        String path = pathSanitizer.sanitize(imageData.location());
+
+        final File file = new File(path);
         return new FileData(Files.asByteSource(file), imageData.getEntityDisplayName(), imageData.contentType().getValue());
     }
 
@@ -139,9 +152,10 @@
     }
 
     private void writeFileToFileSystem(final String fileName, final InputStream uploadedInputStream, final String fileLocation) {
-        try {
-            makeDirectories(fileLocation);
-            FileUtils.copyInputStreamToFile(uploadedInputStream, new File(fileLocation)); // NOSONAR
+        try (BufferedInputStream bis = new BufferedInputStream(uploadedInputStream)) {
+            String sanitizedPath = pathSanitizer.sanitize(fileLocation, bis);
+            makeDirectories(sanitizedPath);
+            FileUtils.copyInputStreamToFile(bis, new File(sanitizedPath)); // NOSONAR
         } catch (final IOException ioException) {
             LOG.warn("writeFileToFileSystem() IOException (logged because cause is not propagated in ContentManagementException)",
                     ioException);
diff --git a/fineract-provider/src/main/resources/application.properties b/fineract-provider/src/main/resources/application.properties
index 1d3fc96..85e69de 100644
--- a/fineract-provider/src/main/resources/application.properties
+++ b/fineract-provider/src/main/resources/application.properties
@@ -39,6 +39,11 @@
 fineract.mode.write-enabled=${FINERACT_MODE_WRITE_ENABLED:true}
 fineract.mode.batch-enabled=${FINERACT_MODE_BATCH_ENABLED:true}
 
+fineract.content.regex-whitelist-enabled=${FINERACT_CONTENT_REGEX_WHITELIST_ENABLED:true}
+fineract.content.regex-whitelist=${FINERACT_CONTENT_REGEX_WHITELIST:.*\\.pdf$,.*\\.doc,.*\\.docx,.*\\.xls,.*\\.xlsx,.*\\.jpg,.*\\.jpeg,.*\\.png}
+fineract.content.mime-whitelist-enabled=${FINERACT_CONTENT_MIME_WHITELIST_ENABLED:true}
+fineract.content.mime-whitelist=${FINERACT_CONTENT_MIME_WHITELIST:application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,image/jpeg,image/png}
+
 management.health.jms.enabled=false
 
 # FINERACT 1296
diff --git a/fineract-provider/src/test/resources/application-test.properties b/fineract-provider/src/test/resources/application-test.properties
index 58981db..47630dd 100644
--- a/fineract-provider/src/test/resources/application-test.properties
+++ b/fineract-provider/src/test/resources/application-test.properties
@@ -37,6 +37,17 @@
 fineract.mode.write-enabled=true
 fineract.mode.batch-enabled=true
 
+fineract.content.regex-whitelist-enabled=true
+fineract.content.regex-whitelist=.*\\.pdf$,.*\\.doc,.*\\.docx,.*\\.xls,.*\\.xlsx,.*\\.jpg,.*\\.jpeg,.*\\.png
+fineract.content.mime-whitelist-enabled=true
+fineract.content.mime-whitelist=application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,image/jpeg,image/png
+fineract.content.filesystem.enabled=true
+fineract.content.filesystem.rootFolder=${user.home}/.fineract
+fineract.content.s3.enabled=false
+fineract.content.s3.bucketName=
+fineract.content.s3.accessName=
+fineract.content.s3.secretName=
+
 management.health.jms.enabled=false
 
 # FINERACT 1296
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/ImageTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/ImageTest.java
index 8bf466b..e58c170 100644
--- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/ImageTest.java
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/ImageTest.java
@@ -18,10 +18,16 @@
  */
 package org.apache.fineract.integrationtests.client;
 
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
 import java.io.File;
 import java.io.IOException;
+import lombok.extern.slf4j.Slf4j;
 import okhttp3.MediaType;
+import okhttp3.MultipartBody;
+import okhttp3.RequestBody;
 import okhttp3.ResponseBody;
+import org.apache.commons.io.IOUtils;
 import org.apache.fineract.client.services.ImagesApi;
 import org.apache.fineract.client.util.Parts;
 import org.junit.jupiter.api.Order;
@@ -36,6 +42,7 @@
  *
  * @author Michael Vorburger.ch
  */
+@Slf4j
 public class ImageTest extends IntegrationTest {
 
     final File testImage = new File(getClass().getResource("/michael.vorburger-crepes.jpg").getFile());
@@ -137,6 +144,116 @@
         ok(fineract().images.delete("clients", clientId));
     }
 
+    @Test
+    @Order(100)
+    void pathTraversalJsp() {
+        final MultipartBody.Part part = createPart("image-text-wrong-content.jsp",
+                "../../../../../../../../../../tmp/image-text-wrong-content.jsp", "image/gif");
+
+        assertThat(part).isNotNull();
+
+        Exception exception = assertThrows(Exception.class, () -> {
+            ok(fineract().images.create("clients", clientId, part));
+        });
+
+        assertThat(exception).isNotNull();
+
+        log.warn("Should not be able to upload a file that doesn't match the indicated content type: {}", exception.getMessage());
+    }
+
+    @Test
+    @Order(101)
+    void gifWithPngExtension() {
+        final MultipartBody.Part part = createPart("image-gif-wrong-extension.png", "image-gif-wrong-extension.png", "image/png");
+
+        assertThat(part).isNotNull();
+
+        Exception exception = assertThrows(Exception.class, () -> {
+            ok(fineract().images.create("clients", clientId, part));
+        });
+
+        assertThat(exception).isNotNull();
+
+        log.warn("Should not be able to upload a gif by just renaming the file extension: {}", exception.getMessage());
+    }
+
+    @Test
+    @Order(102)
+    void gifImage() {
+        final MultipartBody.Part part = createPart("image-gif-correct-extension.gif", "image-gif-correct-extension.gif", "image/png");
+
+        assertThat(part).isNotNull();
+
+        Exception exception = assertThrows(Exception.class, () -> {
+            ok(fineract().images.create("clients", clientId, part));
+        });
+
+        assertThat(exception).isNotNull();
+
+        log.warn("Should not be able to upload a gif it is not whitelisted: {}", exception.getMessage());
+    }
+
+    @Test
+    @Order(103)
+    void pathTraversalJpg() {
+        final MultipartBody.Part part = createPart("michael.vorburger-crepes.jpg",
+                "../../../../../../../../../../tmp/michael.vorburger-crepes.jpg", "image/jpeg");
+
+        assertThat(part).isNotNull();
+
+        Exception exception = assertThrows(Exception.class, () -> {
+            ok(fineract().images.create("clients", clientId, part));
+        });
+
+        assertThat(exception).isNotNull();
+
+        log.warn("Should not be able to upload a file with a forbidden name pattern: {}", exception.getMessage());
+    }
+
+    @Test
+    @Order(104)
+    void pathTraversalWithAbsolutePathJpg() {
+        final MultipartBody.Part part = createPart("michael.vorburger-crepes.jpg", "../17/michael.vorburger-crepes.jpg", "image/jpeg");
+
+        assertThat(part).isNotNull();
+
+        Exception exception = assertThrows(Exception.class, () -> {
+            ok(fineract().images.create("clients", clientId, part));
+        });
+
+        assertThat(exception).isNotNull();
+
+        log.warn("Should not be able to upload a file with a forbidden name pattern: {}", exception.getMessage());
+    }
+
+    @Test
+    @Order(105)
+    void pathTraversalWithAbsolutePathJpg2() {
+        final MultipartBody.Part part = createPart("michael.vorburger-crepes.jpg", "..//17//michael.vorburger-crepes.jpg", "image/jpeg");
+
+        assertThat(part).isNotNull();
+
+        Exception exception = assertThrows(Exception.class, () -> {
+            ok(fineract().images.create("clients", clientId, part));
+        });
+
+        assertThat(exception).isNotNull();
+
+        log.warn("Should not be able to upload a file with a forbidden name pattern: {}", exception.getMessage());
+    }
+
+    private MultipartBody.Part createPart(String fileResource, String fileName, String mediaType) {
+        try {
+            byte[] data = IOUtils.toByteArray(ImageTest.class.getClassLoader().getResourceAsStream(fileResource));
+            RequestBody rb = RequestBody.create(data, MediaType.get(mediaType));
+            return MultipartBody.Part.createFormData("file", fileName, rb);
+        } catch (Exception e) {
+            log.error("Error creating file part.", e);
+        }
+
+        return null;
+    }
+
     interface ImagesApiWithHeadersForTest extends ImagesApi {
 
         @Headers("Accept: text/plain")
diff --git a/integration-tests/src/test/resources/image-gif-correct-extension.gif b/integration-tests/src/test/resources/image-gif-correct-extension.gif
new file mode 100644
index 0000000..89b9516
--- /dev/null
+++ b/integration-tests/src/test/resources/image-gif-correct-extension.gif
Binary files differ
diff --git a/integration-tests/src/test/resources/image-gif-wrong-extension.png b/integration-tests/src/test/resources/image-gif-wrong-extension.png
new file mode 100644
index 0000000..89b9516
--- /dev/null
+++ b/integration-tests/src/test/resources/image-gif-wrong-extension.png
Binary files differ
diff --git a/integration-tests/src/test/resources/image-text-wrong-content.jsp b/integration-tests/src/test/resources/image-text-wrong-content.jsp
new file mode 100644
index 0000000..6e66067
--- /dev/null
+++ b/integration-tests/src/test/resources/image-text-wrong-content.jsp
@@ -0,0 +1,21 @@
+<%--
+
+    Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements. See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership. The ASF licenses this file
+    to you under the Apache License, Version 2.0 (the
+    "License"); you may not use this file except in compliance
+    with the License. You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing,
+    software distributed under the License is distributed on an
+    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+    KIND, either express or implied. See the License for the
+    specific language governing permissions and limitations
+    under the License.
+
+--%>
+<JSP>add JSP CODE here </jsp>
\ No newline at end of file