You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ofbiz.apache.org by jl...@apache.org on 2022/02/19 11:10:21 UTC

[ofbiz-framework] branch release18.12 updated: Improved: Secure the uploads (OFBIZ-12080)

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

jleroux pushed a commit to branch release18.12
in repository https://gitbox.apache.org/repos/asf/ofbiz-framework.git


The following commit(s) were added to refs/heads/release18.12 by this push:
     new 668e801  Improved: Secure the uploads (OFBIZ-12080)
668e801 is described below

commit 668e801ada8ae020e6ccc80c0b27252275f829ed
Author: Jacques Le Roux <ja...@les7arts.com>
AuthorDate: Sat Feb 19 11:37:43 2022 +0100

    Improved: Secure the uploads (OFBIZ-12080)
    
    In security.properties, adds some deniedFileExtensions and improves few comments
    (easier to merge later)
    
    In  DataServices::createFileNoPerm filters file upload to verify the filename
    before the content is checked if necessary
    
    Manually merges SecuredUpload.java based on trunk
---
 .../apache/ofbiz/content/data/DataServices.java    | 32 +++++++++---
 framework/security/config/security.properties      | 17 ++++---
 .../org/apache/ofbiz/security/SecuredUpload.java   | 57 +++++++++++++---------
 3 files changed, 68 insertions(+), 38 deletions(-)

diff --git a/applications/content/src/main/java/org/apache/ofbiz/content/data/DataServices.java b/applications/content/src/main/java/org/apache/ofbiz/content/data/DataServices.java
index 5dd75a7..2bd71c1 100644
--- a/applications/content/src/main/java/org/apache/ofbiz/content/data/DataServices.java
+++ b/applications/content/src/main/java/org/apache/ofbiz/content/data/DataServices.java
@@ -49,6 +49,7 @@ import org.apache.ofbiz.entity.Delegator;
 import org.apache.ofbiz.entity.GenericEntityException;
 import org.apache.ofbiz.entity.GenericValue;
 import org.apache.ofbiz.entity.util.EntityQuery;
+import org.apache.ofbiz.security.SecuredUpload;
 import org.apache.ofbiz.service.DispatchContext;
 import org.apache.ofbiz.service.GenericServiceException;
 import org.apache.ofbiz.service.ModelService;
@@ -195,7 +196,15 @@ public class DataServices {
         return createFileMethod(dctx, context);
     }
 
-    public static Map<String, Object> createFileNoPerm(DispatchContext dctx, Map<String, ? extends Object> rcontext) {
+    public static Map<String, Object> createFileNoPerm(DispatchContext dctx, Map<String, ? extends Object> rcontext) throws IOException,
+            ImageReadException {
+        String originalFileName = (String) rcontext.get("dataResourceName");
+        Delegator delegator = dctx.getDelegator();
+        Locale locale = (Locale) rcontext.get("locale");
+        if (!SecuredUpload.isValidFile(originalFileName, "All", delegator)) {
+            String errorMessage = UtilProperties.getMessage("SecurityUiLabels", "SupportedTextFileFormats", locale);
+            return ServiceUtil.returnError(errorMessage);
+        }
         Map<String, Object> context = UtilMisc.makeMapWritable(rcontext);
         context.put("skipPermissionCheck", "true");
         return createFileMethod(dctx, context);
@@ -254,7 +263,9 @@ public class DataServices {
         if (UtilValidate.isNotEmpty(textData)) {
             try (OutputStreamWriter out = new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8);) {
                 out.write(textData);
-                if (!org.apache.ofbiz.security.SecuredUpload.isValidFile(file.getAbsolutePath(), "Text", delegator)) {
+                // Check if a webshell is not uploaded
+                // TODO I believe the call below to SecuredUpload::isValidFile is now useless because of the same in createFileNoPerm
+                if (!SecuredUpload.isValidFile(file.getAbsolutePath(), "Text", delegator)) {
                     String errorMessage = UtilProperties.getMessage("SecurityUiLabels", "SupportedTextFileFormats", locale);
                     return ServiceUtil.returnError(errorMessage);
                 }
@@ -265,10 +276,11 @@ public class DataServices {
             }
         } else if (binData != null) {
             try {
-                // Check if a webshell is not uploaded
                 Path tempFile = Files.createTempFile(null, null);
                 Files.write(tempFile, binData.array(), StandardOpenOption.APPEND);
-                if (!org.apache.ofbiz.security.SecuredUpload.isValidFile(tempFile.toString(), "All", delegator)) {
+                // Check if a webshell is not uploaded
+                // TODO I believe the call below to SecuredUpload::isValidFile is now useless because of the same in createFileNoPerm
+                if (!SecuredUpload.isValidFile(tempFile.toString(), "All", delegator)) {
                     String errorMessage = UtilProperties.getMessage("SecurityUiLabels", "SupportedFileFormatsIncludingSvg", locale);
                     return ServiceUtil.returnError(errorMessage);
                 }
@@ -461,7 +473,8 @@ public class DataServices {
                 try (OutputStreamWriter out = new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8);) {
                     out.write(textData);
                     // Check if a webshell is not uploaded
-                    if (!org.apache.ofbiz.security.SecuredUpload.isValidFile(file.getAbsolutePath(), "Text", delegator)) {
+                    // TODO I believe the call below to SecuredUpload::isValidFile is now useless because of the same in createFileNoPerm
+                    if (!SecuredUpload.isValidFile(file.getAbsolutePath(), "Text", delegator)) {
                         String errorMessage = UtilProperties.getMessage("SecurityUiLabels", "SupportedTextFileFormats", locale);
                         return ServiceUtil.returnError(errorMessage);
                     }
@@ -473,9 +486,10 @@ public class DataServices {
             } else if (binData != null) {
                 try {
                     // Check if a webshell is not uploaded
+                    // TODO I believe the call below to SecuredUpload::isValidFile is now useless because of the same in createFileNoPerm
                     Path tempFile = Files.createTempFile(null, null);
                     Files.write(tempFile, binData.array(), StandardOpenOption.APPEND);
-                    if (!org.apache.ofbiz.security.SecuredUpload.isValidFile(tempFile.toString(), "Image", delegator)) {
+                    if (!SecuredUpload.isValidFile(tempFile.toString(), "Image", delegator)) {
                         String errorMessage = UtilProperties.getMessage("SecurityUiLabels", "SupportedFileFormatsIncludingSvg", locale);
                         return ServiceUtil.returnError(errorMessage);
                     }
@@ -648,7 +662,8 @@ public class DataServices {
             try (FileOutputStream out = new FileOutputStream(file);) {
                 out.write(imageData);
                 // Check if a webshell is not uploaded
-                if (!org.apache.ofbiz.security.SecuredUpload.isValidFile(file.getAbsolutePath(), "All", delegator)) {
+                // TODO I believe the call below to SecuredUpload::isValidFile is now useless because of the same in createFileNoPerm
+                if (!SecuredUpload.isValidFile(file.getAbsolutePath(), "All", delegator)) {
                     String errorMessage = UtilProperties.getMessage("SecurityUiLabels", "SupportedFileFormatsIncludingSvg", locale);
                     return ServiceUtil.returnError(errorMessage);
                 }
@@ -707,7 +722,8 @@ public class DataServices {
             try (FileOutputStream out = new FileOutputStream(file);) {
                 out.write(imageData);
                 // Check if a webshell is not uploaded
-                if (!org.apache.ofbiz.security.SecuredUpload.isValidFile(file.getAbsolutePath(), "All", delegator)) {
+                // TODO I believe the call below to SecuredUpload::isValidFile is now useless because of the same in createFileNoPerm
+                if (!SecuredUpload.isValidFile(file.getAbsolutePath(), "All", delegator)) {
                     String errorMessage = UtilProperties.getMessage("SecurityUiLabels", "SupportedFileFormatsIncludingSvg", locale);
                     return ServiceUtil.returnError(errorMessage);
                 }
diff --git a/framework/security/config/security.properties b/framework/security/config/security.properties
index eb53037..b11ec6a 100644
--- a/framework/security/config/security.properties
+++ b/framework/security/config/security.properties
@@ -1,4 +1,4 @@
-###############################################################################
+##############################################################################
 # 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
@@ -121,8 +121,9 @@ security.login.cert.allow=true
 # -- pattern for the userlogin id in CN section of certificate
 security.login.cert.pattern=^(\\w*\\s?\\w*)\\W*.*$
 
-# -- use Tomcat SingleSignOn valve
-# -- Tomcat SSO should not be used in a cluster: OFBIZ-10123
+# -- Use Tomcat SingleSignOn valve to allow single sign on (SSO) and single log out (SLO).
+# -- Remember to set security.login.externalLoginKey.enabled to false when using Tomcat SSO.
+# -- Note Tomcat SSO is not implemented for cluster as Tomcat ClusterSingleSignOn is not used: OFBIZ-10123
 security.login.tomcat.sso=false
 
 # -- Hours after which EmailAdressVerification should expire
@@ -156,7 +157,11 @@ security.token.key=security.token.key
 # -- no spaces after commas,no wildcard, can be extended of course...
 host-headers-allowed=localhost,127.0.0.1,demo-trunk.ofbiz.apache.org,demo-stable.ofbiz.apache.org,demo-old.ofbiz.apache.org
 
-# -- By default the SameSite value in SameSiteFilter is strict. This allows to change it to lax if needed
+# -- By default the SameSite value in SameSiteFilter is 'strict'.
+# -- This property allows to change to 'lax' if needed.
+# -- If you use 'lax' we recommend that you set
+# -- org.apache.ofbiz.security.CsrfDefenseStrategy
+# -- for csrf.defense.strategy (see below)
 SameSiteCookieAttribute=
 
 # -- Freemarker TemplateClassResolver option, see OFBIZ-11709.
@@ -203,8 +208,8 @@ templateClassResolver=
 #--
 #-- List of denied files suffixes to be uploaded
 #-- OFBiz of course also check contents...
-deniedFileExtensions=html,htm,php,php1,php2,hph3,php4,php5,asp,aspx,asa,asax,ascx,ashx,asmx,jsp,jspa,jspx,jsw,jsv,jspf,jtml,cfm,cfc,bat,exe,com,dll,\
-                     vbs,js,reg,cgi,htaccess,asis,sh,phtm,shtm,inc,asp,cdx,asa,cer,py,pl,shtml,hta,ps1,tag
+deniedFileExtensions=html,htm,php,php1,php2,hph3,php4,php5,php6,php7,phps,asp,aspx,asa,asax,ascx,ashx,asmx,jsp,jspa,jspx,jsw,jsv,jspf,jtml,cfm,cfc,bat,exe,com,dll,\
+                     vbs,js,reg,cgi,htaccess,asis,sh,phtm,pht,phtml,shtm,inc,asp,cdx,asa,cer,py,pl,shtml,hta,ps1,tag,pgif,htaccess,phar,inc,cgi,wss,do,action
 #--
 #-- The upload vulnerability is only a post-auth (needs a credential with suitable permissions),
 #-- people may like to allow more than what is allowed OOTB
diff --git a/framework/security/src/main/java/org/apache/ofbiz/security/SecuredUpload.java b/framework/security/src/main/java/org/apache/ofbiz/security/SecuredUpload.java
index 35df8c5..364c066 100644
--- a/framework/security/src/main/java/org/apache/ofbiz/security/SecuredUpload.java
+++ b/framework/security/src/main/java/org/apache/ofbiz/security/SecuredUpload.java
@@ -101,19 +101,19 @@ public class SecuredUpload {
     private static final String MODULE = SecuredUpload.class.getName();
     private static final List<String> DENIEDFILEEXTENSIONS = deniedFileExtensions();
     private static final List<String> DENIEDWEBSHELLTOKENS = deniedWebShellTokens();
-    private static final Integer maxLineLength = UtilProperties.getPropertyAsInteger("security", "maxLineLength", 10000);
+    private static final Integer MAXLINELENGTH = UtilProperties.getPropertyAsInteger("security", "maxLineLength", 10000);
 
     public static boolean isValidText(String content, List<String> allowed) throws IOException {
         return DENIEDWEBSHELLTOKENS.stream().allMatch(token -> isValid(content, token, allowed));
     }
 
-    private static boolean isValidFileName(String fileToCheck, Delegator delegator) throws IOException {
+    public static boolean isValidFileName(String fileToCheck, Delegator delegator) throws IOException {
         // Allow all
         if (("true".equalsIgnoreCase(EntityUtilProperties.getPropertyValue("security", "allowAllUploads", delegator)))) {
             return true;
         }
 
-        // Prevent double extensions
+        // Prevents double extensions
         if (StringUtils.countMatches(fileToCheck, ".") > 1) {
             Debug.logError("Double extensions are not allowed for security reason", MODULE);
             return false;
@@ -121,17 +121,30 @@ public class SecuredUpload {
 
         // Check max line length, default 10000
         if (!checkMaxLinesLength(fileToCheck)) {
-            Debug.logError("For security reason lines over " + maxLineLength.toString() + " are not allowed", MODULE);
+            Debug.logError("For security reason lines over " + MAXLINELENGTH.toString() + " are not allowed", MODULE);
             return false;
         }
 
         String imageServerUrl = EntityUtilProperties.getPropertyValue("catalog", "image.management.url", delegator);
         Path p = Paths.get(fileToCheck);
-        String fileName = p.getFileName().toString(); // The file name is the farthest element from the root in the directory hierarchy.
         boolean wrongFile = true;
-        
+
         // Check extensions
-        if (DENIEDFILEEXTENSIONS.contains(FilenameUtils.getExtension(fileToCheck))) {
+        if (p != null && p.getFileName() != null) {
+            String fileName = p.getFileName().toString(); // The file name is the farthest element from the root in the directory hierarchy.
+            String extension = FilenameUtils.getExtension(fileToCheck).toLowerCase();
+            // Prevents null byte in filename
+            if (extension.contains("%00")
+                    || extension.contains("%0a")
+                    || extension.contains("%20")
+                    || extension.contains("%0d%0a")
+                    || extension.contains("/")
+                    || extension.contains("./")
+                    || extension.contains(".")) {
+                Debug.logError("Special bytes in extension are not allowed for security reason", MODULE);
+                return false;
+            }
+            if (DENIEDFILEEXTENSIONS.contains(extension)) {
             Debug.logError("This file extension is not allowed for security reason", MODULE);
             deleteBadFile(fileToCheck);
             return false;
@@ -139,7 +152,8 @@ public class SecuredUpload {
 
             // Check the file and path names
         if (org.apache.commons.lang3.SystemUtils.IS_OS_WINDOWS) {
-            if (fileToCheck.length() > 259) { // More about that: https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation
+            // More about that: https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation
+            if (fileToCheck.length() > 259) {
                 Debug.logError("Uploaded file name too long", MODULE);
             } else if (p.toString().contains(imageServerUrl.replaceAll("/", "\\\\"))) {
                     // TODO check this is still useful in at least 1 case
@@ -161,6 +175,7 @@ public class SecuredUpload {
                 wrongFile = false;
             }
         }
+    }
 
         if (wrongFile) {
             Debug.logError("Uploaded file "
@@ -289,8 +304,9 @@ public class SecuredUpload {
         File file = new File(fileName);
         boolean safeState = false;
         boolean fallbackOnApacheCommonsImaging;
-        try {
+
             if ((file != null) && file.exists() && file.canRead() && file.canWrite()) {
+                try (OutputStream fos = Files.newOutputStream(file.toPath(), StandardOpenOption.WRITE)) {
                 // Get the image format
                 String formatName;
                 ImageInputStream iis = ImageIO.createImageInputStream(file);
@@ -340,7 +356,7 @@ public class SecuredUpload {
                 Graphics bg = sanitizedImage.getGraphics();
                 bg.drawImage(initialSizedImage, 0, 0, null);
                 bg.dispose();
-                OutputStream fos = Files.newOutputStream(file.toPath(), StandardOpenOption.WRITE);
+
                 if (!fallbackOnApacheCommonsImaging) {
                     ImageIO.write(sanitizedImage, formatName, fos);
                 } else {
@@ -348,36 +364,29 @@ public class SecuredUpload {
                     // Handle only formats for which Apache Commons Imaging can successfully write (YES in Write column of the reference link)
                     // the image format. See reference link in the class header
                     switch (formatName) {
-                    case "TIFF": {
+                    case "TIFF":
                         imageParser = new TiffImageParser();
                         break;
-                    }
-                    case "GIF": {
+                    case "GIF":
                         imageParser = new GifImageParser();
                         break;
-                    }
-                    case "PNG": {
+                    case "PNG":
                         imageParser = new PngImageParser();
                         break;
-                    }
-                    case "JPEG": {
+                    case "JPEG":
                         imageParser = new JpegImageParser();
                         break;
-                    }
-                    default: {
+                    default:
                         throw new IOException("Format of the original image " + fileName + " is not supported for write operation !");
                     }
-                    }
                     imageParser.writeImage(sanitizedImage, fos, new HashMap<>());
                 }
                 // Set state flag
-                fos.close(); // This was not correctly handled in the document-upload-protection example, and I did not spot it :/
                 safeState = true;
-            }
         } catch (IOException | ImageReadException | ImageWriteException e) {
-            safeState = false;
             Debug.logWarning(e, "Error during Image file " + fileName + " processing !", MODULE);
         }
+    }
         return safeState;
     }
 
@@ -693,7 +702,7 @@ public class SecuredUpload {
             File file = new File(fileToCheck);
             List<String> lines = FileUtils.readLines(file, Charset.defaultCharset());
             for (String line : lines) {
-                if (line.length() > maxLineLength) {
+                if (line.length() > MAXLINELENGTH) {
                     return false;
                 }
             }