You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@fineract.apache.org by al...@apache.org on 2022/11/22 15:25:37 UTC

[fineract] branch 1.7.1 updated (20c97e64b -> 5b31395a7)

This is an automated email from the ASF dual-hosted git repository.

aleks pushed a change to branch 1.7.1
in repository https://gitbox.apache.org/repos/asf/fineract.git


 discard 20c97e64b FINERACT-1794: Address improvement in file upload APIs
     new 5b31395a7 FINERACT-1794: Address improvement in file upload APIs

This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version.  This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:

 * -- * -- B -- O -- O -- O   (20c97e64b)
            \
             N -- N -- N   refs/heads/1.7.1 (5b31395a7)

You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.

Any revisions marked "omit" are not gone; other references still
refer to them.  Any revisions marked "discard" are gone forever.

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../contentrepository/FileSystemContentPathSanitizer.java               | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)


[fineract] 01/01: FINERACT-1794: Address improvement in file upload APIs

Posted by al...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

aleks pushed a commit to branch 1.7.1
in repository https://gitbox.apache.org/repos/asf/fineract.git

commit 5b31395a7bbc61a7b03517c8ebb008f3ca76d7b3
Author: Aleks <al...@apache.org>
AuthorDate: Tue Nov 22 04:23:02 2022 +0100

    FINERACT-1794: Address improvement in file upload APIs
---
 .../groovy/org.apache.fineract.dependencies.gradle |  32 +++++
 fineract-provider/dependencies.gradle              |   2 +
 .../service/BulkImportWorkbookServiceImpl.java     |   7 +-
 .../contentrepository/ContentPathSanitizer.java    |  28 ++++
 .../ContentRepositoryFactory.java                  |  14 +-
 .../FileSystemContentPathSanitizer.java            | 143 +++++++++++++++++++++
 .../FileSystemContentRepository.java               |  30 +++--
 .../src/main/resources/application.properties      |   5 +
 .../src/test/resources/application-test.properties |  11 ++
 .../integrationtests/client/ImageTest.java         | 117 +++++++++++++++++
 .../test/resources/image-gif-correct-extension.gif | Bin 0 -> 1018890 bytes
 .../test/resources/image-gif-wrong-extension.png   | Bin 0 -> 1018890 bytes
 .../test/resources/image-text-wrong-content.jsp    |  21 +++
 13 files changed, 390 insertions(+), 20 deletions(-)

diff --git a/buildSrc/src/main/groovy/org.apache.fineract.dependencies.gradle b/buildSrc/src/main/groovy/org.apache.fineract.dependencies.gradle
index cc01b28f8..2f5a71316 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 @@ dependencyManagement {
         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 d7c7fc6b6..060796761 100644
--- a/fineract-provider/dependencies.gradle
+++ b/fineract-provider/dependencies.gradle
@@ -52,6 +52,8 @@ dependencies {
             '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 c41c65584..8043125b3 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 @@ public class BulkImportWorkbookServiceImpl implements BulkImportWorkbookService
                 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 @@ public class BulkImportWorkbookServiceImpl implements BulkImportWorkbookService
                             "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 @@ public class BulkImportWorkbookServiceImpl implements BulkImportWorkbookService
                     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 000000000..415532fcd
--- /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 4158fe015..1f5faab30 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 @@ public class ContentRepositoryFactory {
         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 000000000..f9ce7a379
--- /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 19a83e1fe..72614892e 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 class FileSystemContentRepository implements ContentRepository {
 
     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 @@ public class FileSystemContentRepository implements ContentRepository {
     }
 
     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), path, 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 @@ public class FileSystemContentRepository implements ContentRepository {
     }
 
     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 1d3fc966e..85e69dee5 100644
--- a/fineract-provider/src/main/resources/application.properties
+++ b/fineract-provider/src/main/resources/application.properties
@@ -39,6 +39,11 @@ fineract.mode.read-enabled=${FINERACT_MODE_READ_ENABLED:true}
 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 58981db96..47630ddf5 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.read-enabled=true
 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 8bf466bbb..e58c170d6 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 @@ import retrofit2.http.Headers;
  *
  * @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 @@ public class ImageTest extends IntegrationTest {
         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 000000000..89b951662
Binary files /dev/null and b/integration-tests/src/test/resources/image-gif-correct-extension.gif 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 000000000..89b951662
Binary files /dev/null and b/integration-tests/src/test/resources/image-gif-wrong-extension.png 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 000000000..6e660673c
--- /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