This is an automated email from the ASF dual-hosted git repository.
jleroux pushed a commit to branch trunk in repository https://gitbox.apache.org/repos/asf/ofbiz-framework.git The following commit(s) were added to refs/heads/trunk by this push: new e73616c Fixed: Secure the uploads (OFBIZ-12080) e73616c is described below commit e73616caf138030d013300f5ed524246cc3bcff0 Author: Jacques Le Roux <[hidden email]> AuthorDate: Mon Nov 30 19:06:33 2020 +0100 Fixed: Secure the uploads (OFBIZ-12080) 2020/08/10 the OFBiz security team received a security report by Harshit Shukla <[hidden email]>, roughly it was (quoting part of it to simplify): <<I have identified a Remote Code Execution (RCE) Vulnerability. The reason behind this RCE is lack of file extension check at catalog/control/UploadCategoryImage?productCategoryId=CATALOG1_BEST_SELL&pload_file_type=category>> Using this post-auth RCE in OFBiz demos, Harshit was able to get some AWS credentials by uploading a webshell (based on [0]). By security, it was then decided by the Infra and OFBiz security teams to shut down the demos. After discussing the elements reported with Mark J Cox (VP of ASF security team) we in common decided that no CVE was necessary. --- .../ofbiz/content/data/DataResourceWorker.java | 13 +- .../apache/ofbiz/content/data/DataServices.java | 50 +- .../datamodel/data/seed/ContentSeedData.xml | 13 +- .../catalog/category/EditCategory.groovy | 13 +- .../config/EditProductConfigItemContent.groovy | 13 +- .../catalog/imagemanagement/ImageUpload.groovy | 13 +- .../catalog/imagemanagement/SetDefaultImage.groovy | 2 +- .../catalog/product/EditProductContent.groovy | 19 +- .../org/apache/ofbiz/product/image/ScaleImage.java | 28 +- .../ofbiz/product/imagemanagement/FrameImage.java | 9 +- .../imagemanagement/ImageManagementServices.java | 28 +- .../ofbiz/product/product/ProductServices.java | 25 +- .../shipment/FedexShipRequestTemplate.xml.ftl | 4 +- .../FedexSubscriptionRequestTemplate.xml.ftl | 4 +- build.gradle | 7 +- .../java/org/apache/ofbiz/base/util/FileUtil.java | 25 + .../ofbiz/base/util/HttpRequestFileUpload.java | 36 +- .../ofbiz/base/util/template/FreeMarkerWorker.java | 3 +- framework/common/config/SecurityUiLabels.xml | 20 + framework/security/config/security.properties | 43 +- .../org/apache/ofbiz/security/SecuredUpload.java | 548 +++++++++++++++++++++ 21 files changed, 846 insertions(+), 70 deletions(-) diff --git a/applications/content/src/main/java/org/apache/ofbiz/content/data/DataResourceWorker.java b/applications/content/src/main/java/org/apache/ofbiz/content/data/DataResourceWorker.java index 7eef4c3..528b1b7 100644 --- a/applications/content/src/main/java/org/apache/ofbiz/content/data/DataResourceWorker.java +++ b/applications/content/src/main/java/org/apache/ofbiz/content/data/DataResourceWorker.java @@ -551,7 +551,6 @@ public class DataResourceWorker implements org.apache.ofbiz.widget.content.DataR if (parent.exists()) { File[] subs = parent.listFiles(); if (subs != null) { - int length = subs.length; for (File sub : subs) { if (sub.isDirectory()) { dirMap.put(sub.lastModified(), sub); @@ -1000,6 +999,9 @@ public class DataResourceWorker implements org.apache.ofbiz.widget.content.DataR if (!file.isAbsolute()) { throw new GeneralException("File (" + objectInfo + ") is not absolute"); } + if (!file.exists()) { + throw new FileNotFoundException("No file found: " + file.getAbsolutePath()); + } try (InputStreamReader in = new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8)) { UtilIO.copy(in, out); } @@ -1010,6 +1012,9 @@ public class DataResourceWorker implements org.apache.ofbiz.widget.content.DataR sep = "/"; } File file = FileUtil.getFile(prefix + sep + objectInfo); + if (!file.exists()) { + throw new FileNotFoundException("No file found: " + file.getAbsolutePath()); + } try (InputStreamReader in = new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8)) { UtilIO.copy(in, out); } @@ -1020,6 +1025,9 @@ public class DataResourceWorker implements org.apache.ofbiz.widget.content.DataR sep = "/"; } File file = FileUtil.getFile(prefix + sep + objectInfo); + if (!file.exists()) { + throw new FileNotFoundException("No file found: " + file.getAbsolutePath()); + } try (InputStreamReader in = new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8)) { if (Debug.infoOn()) { String enc = in.getEncoding(); @@ -1117,6 +1125,9 @@ public class DataResourceWorker implements org.apache.ofbiz.widget.content.DataR String objectInfo = dataResource.getString("objectInfo"); if (UtilValidate.isNotEmpty(objectInfo)) { File file = DataResourceWorker.getContentFile(dataResourceTypeId, objectInfo, contextRoot); + if (!file.exists()) { + throw new FileNotFoundException("No file found: " + file.getAbsolutePath()); + } return UtilMisc.toMap("stream", Files.newInputStream(file.toPath(), StandardOpenOption.READ), "length", file.length()); } throw new GeneralException("No objectInfo found for FILE type [" + dataResourceTypeId + "]; cannot stream"); 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 cb37a37..5b1ee7b 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 @@ -34,6 +34,7 @@ import java.util.HashMap; import java.util.Locale; import java.util.Map; +import org.apache.commons.imaging.ImageReadException; import org.apache.ofbiz.base.util.Debug; import org.apache.ofbiz.base.util.GeneralException; import org.apache.ofbiz.base.util.UtilDateTime; @@ -198,7 +199,7 @@ public class DataServices { } public static Map<String, Object> createFileMethod(DispatchContext dctx, Map<String, ? extends Object> context) { - //GenericValue dataResource = (GenericValue) context.get("dataResource"); + Delegator delegator = dctx.getDelegator(); String dataResourceTypeId = (String) context.get("dataResourceTypeId"); String objectInfo = (String) context.get("objectInfo"); ByteBuffer binData = (ByteBuffer) context.get("binData"); @@ -250,7 +251,11 @@ public class DataServices { if (UtilValidate.isNotEmpty(textData)) { try (OutputStreamWriter out = new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8);) { out.write(textData); - } catch (IOException e) { + if (!org.apache.ofbiz.security.SecuredUpload.isValidFile(file.getAbsolutePath(), "Text", delegator)) { + String errorMessage = UtilProperties.getMessage("SecurityUiLabels", "SupportedTextFileFormats", locale); + return ServiceUtil.returnError(errorMessage); + } + } catch (IOException | ImageReadException e) { Debug.logWarning(e, MODULE); return ServiceUtil.returnError(UtilProperties.getMessage(RESOURCE, "ContentUnableWriteCharacterDataToFile", UtilMisc.toMap("fileName", file.getAbsolutePath()), locale)); @@ -260,7 +265,13 @@ public class DataServices { RandomAccessFile out = new RandomAccessFile(file, "rw"); out.write(binData.array()); out.close(); - } catch (FileNotFoundException e) { + // Check if a webshell is not uploaded + if (!org.apache.ofbiz.security.SecuredUpload.isValidFile(file.getAbsolutePath(), "All", delegator)) { + String errorMessage = UtilProperties.getMessage("SecurityUiLabels", "SupportedFileIncludingSvgFormats", locale); + return ServiceUtil.returnError(errorMessage); + } + + } catch (FileNotFoundException | ImageReadException e) { Debug.logError(e, MODULE); return ServiceUtil.returnError(UtilProperties.getMessage(RESOURCE, "ContentUnableToOpenFileForWriting", UtilMisc.toMap("fileName", file.getAbsolutePath()), locale)); @@ -404,6 +415,7 @@ public class DataServices { } public static Map<String, Object> updateFileMethod(DispatchContext dctx, Map<String, ? extends Object> context) throws GenericServiceException { + Delegator delegator = dctx.getDelegator(); Map<String, Object> result = new HashMap<>(); Locale locale = (Locale) context.get("locale"); String dataResourceTypeId = (String) context.get("dataResourceTypeId"); @@ -442,7 +454,11 @@ public class DataServices { if (UtilValidate.isNotEmpty(textData)) { try (OutputStreamWriter out = new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8);) { out.write(textData); - } catch (IOException e) { + if (!org.apache.ofbiz.security.SecuredUpload.isValidFile(file.getAbsolutePath(), "Text", delegator)) { + String errorMessage = UtilProperties.getMessage("SecurityUiLabels", "SupportedTextFileFormats", locale); + return ServiceUtil.returnError(errorMessage); + } + } catch (IOException | ImageReadException e) { Debug.logWarning(e, MODULE); return ServiceUtil.returnError(UtilProperties.getMessage(RESOURCE, "ContentUnableWriteCharacterDataToFile", UtilMisc.toMap("fileName", file.getAbsolutePath()), locale)); @@ -453,7 +469,12 @@ public class DataServices { out.setLength(binData.array().length); out.write(binData.array()); out.close(); - } catch (FileNotFoundException e) { + // Check if a webshell is not uploaded + if (!org.apache.ofbiz.security.SecuredUpload.isValidFile(file.getAbsolutePath(), "All", delegator)) { + String errorMessage = UtilProperties.getMessage("SecurityUiLabels", "SupportedFileIncludingSvgFormats", locale); + return ServiceUtil.returnError(errorMessage); + } + } catch (FileNotFoundException | ImageReadException e) { Debug.logError(e, MODULE); return ServiceUtil.returnError(UtilProperties.getMessage(RESOURCE, "ContentUnableToOpenFileForWriting", UtilMisc.toMap("fileName", file.getAbsolutePath()), locale)); @@ -589,12 +610,14 @@ public class DataServices { public static Map<String, Object> createBinaryFileMethod(DispatchContext dctx, Map<String, ? extends Object> context) throws GenericServiceException { + Delegator delegator = dctx.getDelegator(); Map<String, Object> result = new HashMap<>(); GenericValue dataResource = (GenericValue) context.get("dataResource"); String dataResourceTypeId = (String) dataResource.get("dataResourceTypeId"); String objectInfo = (String) dataResource.get("objectInfo"); byte[] imageData = (byte[]) context.get("imageData"); String rootDir = (String) context.get("rootDir"); + Locale locale = (Locale) context.get("locale"); File file = null; if (Debug.infoOn()) { Debug.logInfo("in createBinaryFileMethod, dataResourceTypeId:" + dataResourceTypeId, MODULE); @@ -614,10 +637,15 @@ public class DataServices { if (imageData != null && imageData.length > 0) { 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)) { + String errorMessage = UtilProperties.getMessage("SecurityUiLabels", "SupportedFileIncludingSvgFormats", locale); + return ServiceUtil.returnError(errorMessage); + } if (Debug.infoOn()) { Debug.logInfo("in createBinaryFileMethod, length:" + file.length(), MODULE); } - } catch (IOException e) { + } catch (IOException | ImageReadException e) { Debug.logWarning(e, MODULE); throw new GenericServiceException(e.getMessage()); } @@ -641,12 +669,14 @@ public class DataServices { public static Map<String, Object> updateBinaryFileMethod(DispatchContext dctx, Map<String, ? extends Object> context) throws GenericServiceException { + Delegator delegator = dctx.getDelegator(); Map<String, Object> result = new HashMap<>(); GenericValue dataResource = (GenericValue) context.get("dataResource"); String dataResourceTypeId = (String) dataResource.get("dataResourceTypeId"); String objectInfo = (String) dataResource.get("objectInfo"); byte[] imageData = (byte[]) context.get("imageData"); String rootDir = (String) context.get("rootDir"); + Locale locale = (Locale) context.get("locale"); File file = null; if (Debug.infoOn()) { Debug.logInfo("in updateBinaryFileMethod, dataResourceTypeId:" + dataResourceTypeId, MODULE); @@ -666,10 +696,16 @@ public class DataServices { if (imageData != null && imageData.length > 0) { 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)) { + String errorMessage = UtilProperties.getMessage("SecurityUiLabels", "SupportedFileIncludingSvgFormats", locale); + return ServiceUtil.returnError(errorMessage); + } + if (Debug.infoOn()) { Debug.logInfo("in updateBinaryFileMethod, length:" + file.length(), MODULE); } - } catch (IOException e) { + } catch (IOException | ImageReadException e) { Debug.logWarning(e, MODULE); throw new GenericServiceException(e.getMessage()); } diff --git a/applications/datamodel/data/seed/ContentSeedData.xml b/applications/datamodel/data/seed/ContentSeedData.xml index f6b1bf1..fcaa664 100644 --- a/applications/datamodel/data/seed/ContentSeedData.xml +++ b/applications/datamodel/data/seed/ContentSeedData.xml @@ -407,7 +407,8 @@ under the License. <MimeType mimeTypeId="audio/basic" description="Basic Audio"/> <MimeType mimeTypeId="audio/mpeg" description="MPEG Audio"/> <MimeType mimeTypeId="audio/x-ms-wax" description="WAX Audio"/> - <MimeType mimeTypeId="audio/x-ms-wma" description="WMA Audio"/> + <MimeType mimeTypeId="audio/ogg" description="OGG Audio"/> + <MimeType mimeTypeId="audio/wav" description="WAV Audio"/> <!-- image mime types --> <MimeType mimeTypeId="image/jpeg" description="JPEG/JPG Image"/> @@ -415,6 +416,8 @@ under the License. <MimeType mimeTypeId="image/gif" description="GIF Image"/> <MimeType mimeTypeId="image/tiff" description="TIFF Image"/> <MimeType mimeTypeId="image/png" description="PNG Image"/> + <MimeType mimeTypeId="image/svg+xml" description="Image SVG"/> + <!-- message mime types --> <MimeType mimeTypeId="message/external-body" description="External Body Message"/> @@ -473,6 +476,7 @@ under the License. <FileExtension fileExtensionId="tif" mimeTypeId="image/tiff"/> <FileExtension fileExtensionId="tiff" mimeTypeId="image/tiff"/> <FileExtension fileExtensionId="gif" mimeTypeId="image/gif"/> + <FileExtension fileExtensionId="svg" mimeTypeId="image/svg+xml"/> <FileExtension fileExtensionId="png" mimeTypeId="image/png"/> <FileExtension fileExtensionId="mp4" mimeTypeId="video/mp4"/> <FileExtension fileExtensionId="m4v" mimeTypeId="video/mp4"/> @@ -497,6 +501,11 @@ under the License. <FileExtension fileExtensionId="wmx" mimeTypeId="video/x-ms-wmx"/> <FileExtension fileExtensionId="zip" mimeTypeId="application/zip"/> <FileExtension fileExtensionId="vcf" mimeTypeId="text/x-vcard"/> + <FileExtension fileExtensionId="wav" mimeTypeId="audio/wav"/> + <FileExtension fileExtensionId="mpeg" mimeTypeId="audio/mpeg"/> + <FileExtension fileExtensionId="ogg" mimeTypeId="audio/ogg"/> + <FileExtension fileExtensionId="dflt" mimeTypeId="application/octet-stream"/> + <!-- Registered MIME Types: MEDIA TYPES @@ -2101,4 +2110,4 @@ mimeTypeId="text/html" createdDate="2004-08-06 11:40:15.818"/> <Content contentId="NOTEXTFOUND" contentTypeId="DOCUMENT" dataResourceId="NOTEXTFOUND" contentName="No Text Found" description="No Text Found" createdDate="2004-08-06 11:40:15.818"/> -</entity-engine-xml> \ No newline at end of file +</entity-engine-xml> diff --git a/applications/product/groovyScripts/catalog/category/EditCategory.groovy b/applications/product/groovyScripts/catalog/category/EditCategory.groovy index 47d690f..f220fd4 100644 --- a/applications/product/groovyScripts/catalog/category/EditCategory.groovy +++ b/applications/product/groovyScripts/catalog/category/EditCategory.groovy @@ -78,7 +78,16 @@ if (fileType) { uploadObject = new HttpRequestFileUpload() uploadObject.setOverrideFilename(defaultFileName) uploadObject.setSavePath(imageServerPath + "/" + filePathPrefix) - uploadObject.doUpload(request) + if (!uploadObject.doUpload(request, "Image")) { + try { + (new File(imageServerPath + "/" + filePathPrefix, defaultFileName)).delete() + } catch (Exception e) { + logError(e, "error deleting existing file (not necessarily a problem, except if it's a webshell!)") + } + String errorMessage = UtilProperties.getMessage("SecurityUiLabels","SupportedImageFormats", locale) + logError(errorMessage) + return error(errorMessage) + } clientFileName = uploadObject.getFilename() if (clientFileName) { @@ -104,7 +113,7 @@ if (fileType) { try { file1.delete() } catch (Exception e) { - logError(e, "error deleting existing file (not neccessarily a problem)") + logError(e, "error deleting existing file (not necessarily a problem, except if it's a webshell!)") } file.renameTo(file1) } catch (Exception e) { diff --git a/applications/product/groovyScripts/catalog/config/EditProductConfigItemContent.groovy b/applications/product/groovyScripts/catalog/config/EditProductConfigItemContent.groovy index 034be19..c01531e 100644 --- a/applications/product/groovyScripts/catalog/config/EditProductConfigItemContent.groovy +++ b/applications/product/groovyScripts/catalog/config/EditProductConfigItemContent.groovy @@ -91,7 +91,16 @@ if (fileType) { uploadObject = new HttpRequestFileUpload() uploadObject.setOverrideFilename(defaultFileName) uploadObject.setSavePath(imageServerPath + "/" + filePathPrefix) - uploadObject.doUpload(request) + if (!uploadObject.doUpload(request, "Image")) { + try { + (new File(imageServerPath + "/" + filePathPrefix, defaultFileName)).delete() + } catch (Exception e) { + logError(e, "error deleting existing file (not necessarily a problem, except if it's a webshell!)") + } + String errorMessage = UtilProperties.getMessage("SecurityUiLabels","SupportedImageFormats", locale) + logError(errorMessage) + return error(errorMessage) + } clientFileName = uploadObject.getFilename() if (clientFileName) { @@ -114,7 +123,7 @@ if (fileType) { try { file1.delete() } catch (Exception e) { - logError(e, "error deleting existing file (not neccessarily a problem)") + logError(e, "error deleting existing file (not necessarily a problem, except if it's a webshell!)") } file.renameTo(file1) } catch (Exception e) { diff --git a/applications/product/groovyScripts/catalog/imagemanagement/ImageUpload.groovy b/applications/product/groovyScripts/catalog/imagemanagement/ImageUpload.groovy index 6a18c67..15253fc 100644 --- a/applications/product/groovyScripts/catalog/imagemanagement/ImageUpload.groovy +++ b/applications/product/groovyScripts/catalog/imagemanagement/ImageUpload.groovy @@ -91,7 +91,16 @@ if (fileType) { uploadObject = new HttpRequestFileUpload() uploadObject.setOverrideFilename(defaultFileName) uploadObject.setSavePath(imageServerPath + "/" + filePathPrefix) - uploadObject.doUpload(request) + if (!uploadObject.doUpload(request, "Image")) { + try { + (new File(imageServerPath + "/" + filePathPrefix, defaultFileName)).delete() + } catch (Exception e) { + logError(e, "error deleting existing file (not necessarily a problem, except if it's a webshell!)") + } + String errorMessage = UtilProperties.getMessage("SecurityUiLabels","SupportedImageFormats", locale) + logError(errorMessage) + return error(errorMessage) + } clientFileName = uploadObject.getFilename() if (clientFileName) { @@ -117,7 +126,7 @@ if (fileType) { try { file1.delete() } catch (Exception e) { - logError(e, "error deleting existing file (not neccessarily a problem)") + logError(e, "error deleting existing file (not necessarily a problem, except if it's a webshell!)") } file.renameTo(file1) } catch (Exception e) { diff --git a/applications/product/groovyScripts/catalog/imagemanagement/SetDefaultImage.groovy b/applications/product/groovyScripts/catalog/imagemanagement/SetDefaultImage.groovy index 446e74c..3f4b169 100644 --- a/applications/product/groovyScripts/catalog/imagemanagement/SetDefaultImage.groovy +++ b/applications/product/groovyScripts/catalog/imagemanagement/SetDefaultImage.groovy @@ -155,7 +155,7 @@ if (fileType) { } } } catch (Exception e) { - logError(e, "error deleting existing file (not neccessarily a problem)") + logError(e, "error deleting existing file (not necessarily a problem, except if it's a webshell!)") } file.renameTo(file1) } catch (Exception e) { diff --git a/applications/product/groovyScripts/catalog/product/EditProductContent.groovy b/applications/product/groovyScripts/catalog/product/EditProductContent.groovy index 71f3c0c..7821067 100644 --- a/applications/product/groovyScripts/catalog/product/EditProductContent.groovy +++ b/applications/product/groovyScripts/catalog/product/EditProductContent.groovy @@ -23,6 +23,8 @@ import org.apache.ofbiz.base.util.string.* import org.apache.ofbiz.entity.util.EntityUtilProperties import org.apache.ofbiz.product.image.ScaleImage +import org.apache.commons.io.FileUtils + context.nowTimestampString = UtilDateTime.nowTimestamp().toString() // make the image file formats @@ -90,7 +92,16 @@ if (fileType) { uploadObject = new HttpRequestFileUpload() uploadObject.setOverrideFilename(defaultFileName) uploadObject.setSavePath(imageServerPath + "/" + filePathPrefix) - uploadObject.doUpload(request) + if (!uploadObject.doUpload(request, "Image")) { + try { + (new File(imageServerPath + "/" + filePathPrefix, defaultFileName)).delete() + } catch (Exception e) { + logError(e, "error deleting existing file (not necessarily a problem, except if it's a webshell!)") + } + String errorMessage = UtilProperties.getMessage("SecurityUiLabels","SupportedImageFormats", locale) + logError(errorMessage) + return error(errorMessage) + } clientFileName = uploadObject.getFilename() if (clientFileName) { @@ -130,11 +141,13 @@ if (fileType) { } else { File[] files = targetDir.listFiles() for(File file : files) { - if (file.isFile() && !file.getName().equals(defaultFileName) && file.getName().startsWith(productId + ".")) file.delete() + if (file.isFile() && !file.getName().equals(defaultFileName) && file.getName().startsWith(productId + ".")) { + file.delete() + } } } } catch (Exception e) { - logError(e, "error deleting existing file (not neccessarily a problem)") + logError(e, "error deleting existing file (not necessarily a problem, except if it's a webshell!)") } file.renameTo(file1) } catch (Exception e) { diff --git a/applications/product/src/main/java/org/apache/ofbiz/product/image/ScaleImage.java b/applications/product/src/main/java/org/apache/ofbiz/product/image/ScaleImage.java index 637b112..cb8587d 100644 --- a/applications/product/src/main/java/org/apache/ofbiz/product/image/ScaleImage.java +++ b/applications/product/src/main/java/org/apache/ofbiz/product/image/ScaleImage.java @@ -29,6 +29,7 @@ import java.util.Map; import javax.imageio.ImageIO; +import org.apache.commons.imaging.ImageReadException; import org.apache.ofbiz.base.location.FlexibleLocation; import org.apache.ofbiz.base.util.Debug; import org.apache.ofbiz.base.util.UtilGenerics; @@ -74,10 +75,10 @@ public class ScaleImage { * @throws JDOMException Errors occur in parsing */ public static Map<String, Object> scaleImageInAllSize(Map<String, ? extends Object> context, String filenameToUse, - String viewType, String viewNumber) - throws IllegalArgumentException, ImagingOpException, IOException, JDOMException { + String viewType, String viewNumber) throws IllegalArgumentException, ImagingOpException, IOException, JDOMException { /* VARIABLES */ + Delegator delegator = (Delegator) context.get("delegator"); Locale locale = (Locale) context.get("locale"); int index; @@ -221,19 +222,23 @@ public class ScaleImage { // write new image try { - ImageIO.write(bufNewImg, imgExtension, new File(imageServerPath + "/" + newFileLocation + "." + imgExtension)); + String fileToCheck = imageServerPath + "/" + newFileLocation + "." + imgExtension; + ImageIO.write(bufNewImg, imgExtension, new File(fileToCheck)); + if (!org.apache.ofbiz.security.SecuredUpload.isValidFile(fileToCheck, "Image", delegator)) { + String errorMessage = UtilProperties.getMessage("SecurityUiLabels", "SupportedImageFormats", locale); + return ServiceUtil.returnError(errorMessage); + } } catch (IllegalArgumentException e) { String errMsg = UtilProperties.getMessage(RESOURCE, "ScaleImage.one_parameter_is_null", locale) + e.toString(); Debug.logError(errMsg, MODULE); result.put(ModelService.ERROR_MESSAGE, errMsg); return result; - } catch (IOException e) { + } catch (IOException | ImageReadException e) { String errMsg = UtilProperties.getMessage(RESOURCE, "ScaleImage.error_occurs_during_writing", locale) + e.toString(); Debug.logError(errMsg, MODULE); result.put(ModelService.ERROR_MESSAGE, errMsg); return result; } - // Save each Url if (SIZE_TYPE_LIST.contains(sizeType)) { String imageUrl = imageUrlPrefix + "/" + newFileLocation + "." + imgExtension; @@ -257,10 +262,10 @@ public class ScaleImage { } public static Map<String, Object> scaleImageManageInAllSize(Map<String, ? extends Object> context, String filenameToUse, - String viewType, String viewNumber, String imageType) - throws IllegalArgumentException, ImagingOpException, IOException, JDOMException { + String viewType, String viewNumber, String imageType) throws IllegalArgumentException, ImagingOpException, IOException, JDOMException { /* VARIABLES */ + Delegator delegator = (Delegator) context.get("delegator"); Locale locale = (Locale) context.get("locale"); List<String> sizeTypeList = null; if (UtilValidate.isNotEmpty(imageType)) { @@ -395,13 +400,18 @@ public class ScaleImage { // write new image try { - ImageIO.write(bufNewImg, imgExtension, new File(imageServerPath + "/" + newFilePathPrefix + filenameToUse)); + String fileToCheck = imageServerPath + "/" + newFileLocation + "." + imgExtension; + ImageIO.write(bufNewImg, imgExtension, new File(fileToCheck)); + if (!org.apache.ofbiz.security.SecuredUpload.isValidFile(fileToCheck, "Image", delegator)) { + String errorMessage = UtilProperties.getMessage("SecurityUiLabels", "SupportedImageFormats", locale); + return ServiceUtil.returnError(errorMessage); + } } catch (IllegalArgumentException e) { String errMsg = UtilProperties.getMessage(RESOURCE, "ScaleImage.one_parameter_is_null", locale) + e.toString(); Debug.logError(errMsg, MODULE); result.put(ModelService.ERROR_MESSAGE, errMsg); return result; - } catch (IOException e) { + } catch (IOException | ImageReadException e) { String errMsg = UtilProperties.getMessage(RESOURCE, "ScaleImage.error_occurs_during_writing", locale) + e.toString(); Debug.logError(errMsg, MODULE); result.put(ModelService.ERROR_MESSAGE, errMsg); diff --git a/applications/product/src/main/java/org/apache/ofbiz/product/imagemanagement/FrameImage.java b/applications/product/src/main/java/org/apache/ofbiz/product/imagemanagement/FrameImage.java index d240b3f..d6b9fec 100644 --- a/applications/product/src/main/java/org/apache/ofbiz/product/imagemanagement/FrameImage.java +++ b/applications/product/src/main/java/org/apache/ofbiz/product/imagemanagement/FrameImage.java @@ -41,6 +41,7 @@ import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import javax.swing.ImageIcon; +import org.apache.commons.imaging.ImageReadException; import org.apache.ofbiz.base.util.Debug; import org.apache.ofbiz.base.util.FileUtil; import org.apache.ofbiz.base.util.UtilDateTime; @@ -271,6 +272,7 @@ public class FrameImage { LocalDispatcher dispatcher = (LocalDispatcher) request.getAttribute("dispatcher"); Delegator delegator = dispatcher.getDelegator(); HttpSession session = request.getSession(); + Locale locale = request.getLocale(); GenericValue userLogin = (GenericValue) session.getAttribute("userLogin"); Map<String, ? extends Object> context = UtilGenerics.cast(request.getParameterMap()); @@ -316,6 +318,11 @@ public class FrameImage { RandomAccessFile out = new RandomAccessFile(file, "rw"); out.write(imageData.array()); out.close(); + if (!org.apache.ofbiz.security.SecuredUpload.isValidFile(file.getAbsolutePath(), "Image", delegator)) { + String errorMessage = UtilProperties.getMessage("SecurityUiLabels", "SupportedFileIncludingSvgFormats", locale); + request.setAttribute("_ERROR_MESSAGE_", errorMessage); + return "error"; + } //create dataResource Map<String, Object> dataResourceCtx = new HashMap<>(); @@ -348,7 +355,7 @@ public class FrameImage { return "error"; } contentId = contentResult.get("contentId").toString(); - } catch (GenericServiceException | IOException gse) { + } catch (GenericServiceException | IOException | ImageReadException gse) { request.setAttribute("_ERROR_MESSAGE_", gse.getMessage()); return "error"; } diff --git a/applications/product/src/main/java/org/apache/ofbiz/product/imagemanagement/ImageManagementServices.java b/applications/product/src/main/java/org/apache/ofbiz/product/imagemanagement/ImageManagementServices.java index b19369b..8bcf6f7 100644 --- a/applications/product/src/main/java/org/apache/ofbiz/product/imagemanagement/ImageManagementServices.java +++ b/applications/product/src/main/java/org/apache/ofbiz/product/imagemanagement/ImageManagementServices.java @@ -34,6 +34,7 @@ import java.util.Map; import javax.imageio.ImageIO; +import org.apache.commons.imaging.ImageReadException; import org.apache.ofbiz.base.location.FlexibleLocation; import org.apache.ofbiz.base.util.Debug; import org.apache.ofbiz.base.util.UtilDateTime; @@ -68,7 +69,7 @@ public class ImageManagementServices { private static String imagePath; public static Map<String, Object> addMultipleuploadForProduct(DispatchContext dctx, - Map<String, ? extends Object> context) { + Map<String, ? extends Object> context) throws ImageReadException { Map<String, Object> result = new HashMap<>(); LocalDispatcher dispatcher = dctx.getDispatcher(); @@ -140,7 +141,8 @@ public class ImageManagementServices { } } - File file = new File(imageServerPath + "/" + productId + "/" + uploadFileName); + String fileToCheck = imageServerPath + "/" + productId + "/" + uploadFileName; + File file = new File(fileToCheck); String imageName = null; imagePath = imageServerPath + "/" + productId + "/" + uploadFileName; file = checkExistsImage(file); @@ -157,6 +159,10 @@ public class ImageManagementServices { RandomAccessFile out = new RandomAccessFile(file, "rw"); out.write(imageData.array()); out.close(); + if (!org.apache.ofbiz.security.SecuredUpload.isValidFile(fileToCheck, "Image", delegator)) { + String errorMessage = UtilProperties.getMessage("SecurityUiLabels", "SupportedImageFormats", locale); + return ServiceUtil.returnError(errorMessage); + } } catch (FileNotFoundException e) { Debug.logError(e, MODULE); return ServiceUtil.returnError(UtilProperties.getMessage(RES_ERROR, @@ -169,13 +175,19 @@ public class ImageManagementServices { } // Scale Image in different sizes if (UtilValidate.isNotEmpty(imageResize)) { - File fileOriginal = new File(imageServerPath + "/" + productId + "/" + imageName); + fileToCheck = imageServerPath + "/" + productId + "/" + imageName; + File fileOriginal = new File(fileToCheck); fileOriginal = checkExistsImage(fileOriginal); try { RandomAccessFile outFile = new RandomAccessFile(fileOriginal, "rw"); outFile.write(imageData.array()); outFile.close(); + if (!org.apache.ofbiz.security.SecuredUpload.isValidFile(fileToCheck, "Image", delegator)) { + String errorMessage = UtilProperties.getMessage("SecurityUiLabels", "SupportedImageFormats", locale); + return ServiceUtil.returnError(errorMessage); + } + } catch (FileNotFoundException e) { Debug.logError(e, MODULE); return ServiceUtil.returnError(UtilProperties.getMessage(RES_ERROR, @@ -504,7 +516,7 @@ public class ImageManagementServices { } public static Map<String, Object> createContentThumbnail(DispatchContext dctx, Map<String, ? extends Object> context, - GenericValue userLogin, ByteBuffer imageData, String productId, String imageName) { + GenericValue userLogin, ByteBuffer imageData, String productId, String imageName) throws ImageReadException { Map<String, Object> result = new HashMap<>(); LocalDispatcher dispatcher = dctx.getDispatcher(); Delegator delegator = dctx.getDelegator(); @@ -554,11 +566,17 @@ public class ImageManagementServices { } result.put("filenameToUseThumb", filenameToUseThumb); // Create image file thumbnail to folder product id. - File fileOriginalThumb = new File(imageServerPath + "/" + productId + "/" + filenameToUseThumb); + String fileToCheck = imageServerPath + "/" + productId + "/" + filenameToUseThumb; + File fileOriginalThumb = new File(fileToCheck); try { RandomAccessFile outFileThumb = new RandomAccessFile(fileOriginalThumb, "rw"); outFileThumb.write(imageData.array()); outFileThumb.close(); + if (!org.apache.ofbiz.security.SecuredUpload.isValidFile(fileToCheck, "Image", delegator)) { + String errorMessage = UtilProperties.getMessage("SecurityUiLabels", "SupportedImageFormats", locale); + return ServiceUtil.returnError(errorMessage); + } + } catch (FileNotFoundException e) { Debug.logError(e, MODULE); return ServiceUtil.returnError(UtilProperties.getMessage(RES_ERROR, diff --git a/applications/product/src/main/java/org/apache/ofbiz/product/product/ProductServices.java b/applications/product/src/main/java/org/apache/ofbiz/product/product/ProductServices.java index fbb6c6b..1ee5804 100644 --- a/applications/product/src/main/java/org/apache/ofbiz/product/product/ProductServices.java +++ b/applications/product/src/main/java/org/apache/ofbiz/product/product/ProductServices.java @@ -37,6 +37,7 @@ import java.util.Map; import java.util.Map.Entry; import java.util.Set; +import org.apache.commons.imaging.ImageReadException; import org.apache.ofbiz.base.util.Debug; import org.apache.ofbiz.base.util.UtilDateTime; import org.apache.ofbiz.base.util.UtilGenerics; @@ -966,7 +967,7 @@ public class ProductServices { } public static Map<String, Object> addAdditionalViewForProduct(DispatchContext dctx, - Map<String, ? extends Object> context) { + Map<String, ? extends Object> context) throws ImageReadException { LocalDispatcher dispatcher = dctx.getDispatcher(); Delegator delegator = dctx.getDelegator(); @@ -1070,11 +1071,16 @@ public class ProductServices { } // Write try { - File file = new File(imageServerPath + "/" + fileLocation + "." + extension.getString("fileExtensionId")); + String fileToCheck = imageServerPath + "/" + fileLocation + "." + extension.getString("fileExtensionId"); + File file = new File(fileToCheck); try { - RandomAccessFile out = new RandomAccessFile(file, "rw"); + RandomAccessFile out = new RandomAccessFile(fileToCheck, "rw"); out.write(imageData.array()); out.close(); + if (!org.apache.ofbiz.security.SecuredUpload.isValidFile(fileToCheck, "Image", delegator)) { + String errorMessage = UtilProperties.getMessage("SecurityUiLabels", "SupportedImageFormats", locale); + return ServiceUtil.returnError(errorMessage); + } } catch (FileNotFoundException e) { Debug.logError(e, MODULE); return ServiceUtil.returnError(UtilProperties.getMessage(RESOURCE, @@ -1341,8 +1347,7 @@ public class ProductServices { String filePathPrefix = ""; String filenameToUse = fileLocation; if (fileLocation.lastIndexOf('/') != -1) { - filePathPrefix = fileLocation.substring(0, fileLocation.lastIndexOf('/') + 1); // adding 1 to include - // the trailing slash + filePathPrefix = fileLocation.substring(0, fileLocation.lastIndexOf('/') + 1); // adding 1 to include the trailing slash filenameToUse = fileLocation.substring(fileLocation.lastIndexOf('/') + 1); } @@ -1369,17 +1374,23 @@ public class ProductServices { } } - File file = new File(imageServerPath + "/" + filePathPrefix + filenameToUse); + String fileToCheck = imageServerPath + "/" + filePathPrefix + filenameToUse; + File file = new File(fileToCheck); try { RandomAccessFile out = new RandomAccessFile(file, "rw"); out.write(imageData.array()); out.close(); + if (!org.apache.ofbiz.security.SecuredUpload.isValidFile(fileToCheck, "Image", delegator)) { + String errorMessage = UtilProperties.getMessage("SecurityUiLabels", "SupportedImageFormats", locale); + return ServiceUtil.returnError(errorMessage); + } + } catch (FileNotFoundException e) { Debug.logError(e, MODULE); return ServiceUtil.returnError(UtilProperties.getMessage(RESOURCE, "ProductImageViewUnableWriteFile", UtilMisc.toMap("fileName", file.getAbsolutePath()), locale)); - } catch (IOException e) { + } catch (IOException | ImageReadException e) { Debug.logError(e, MODULE); return ServiceUtil.returnError(UtilProperties.getMessage(RESOURCE, "ProductImageViewUnableWriteBinaryData", UtilMisc.toMap("fileName", file.getAbsolutePath()), locale)); diff --git a/applications/product/template/shipment/FedexShipRequestTemplate.xml.ftl b/applications/product/template/shipment/FedexShipRequestTemplate.xml.ftl index b13f985..90610cf 100644 --- a/applications/product/template/shipment/FedexShipRequestTemplate.xml.ftl +++ b/applications/product/template/shipment/FedexShipRequestTemplate.xml.ftl @@ -20,7 +20,7 @@ under the License. --> - <#-- FreeMarker template for Fedex FDXShipRequest --> + <#-- FTL emplate for Fedex FDXShipRequest --> <FDXShipRequest xmlns:api="http://www.fedex.com/fsmapi" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FDXShipRequest.xsd"> <RequestHeader> @@ -121,4 +121,4 @@ </FDXShipRequest> -</#compress> \ No newline at end of file +</#compress> diff --git a/applications/product/template/shipment/FedexSubscriptionRequestTemplate.xml.ftl b/applications/product/template/shipment/FedexSubscriptionRequestTemplate.xml.ftl index e33f456..6f29e99 100644 --- a/applications/product/template/shipment/FedexSubscriptionRequestTemplate.xml.ftl +++ b/applications/product/template/shipment/FedexSubscriptionRequestTemplate.xml.ftl @@ -20,7 +20,7 @@ under the License. --> - <#-- FreeMarker template for Fedex FDXSubscriptionRequest --> + <#-- FTL template for Fedex FDXSubscriptionRequest --> <FDXSubscriptionRequest xmlns:api="http://www.fedex.com/fsmapi" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FDXSubscriptionRequest.xsd"> <RequestHeader> @@ -34,7 +34,7 @@ <FaxNumber>${FaxNumber?xml}</FaxNumber> </#if> <#if EMailAddress??> - <#-- Freemarker has a problem with the E-MailAddress tag name, so the opening and closing tags need to be wrapped in the noparse directive. --> + <#-- Free-marker has a problem with the E-MailAddress tag name, so the opening and closing tags need to be wrapped in the noparse directive. --> <#noparse><E-MailAddress></#noparse>${EMailAddress?xml}<#noparse></E-MailAddress></#noparse> </#if> </Contact> diff --git a/build.gradle b/build.gradle index b28c6ff..c30c108 100644 --- a/build.gradle +++ b/build.gradle @@ -171,9 +171,11 @@ dependencies { implementation 'commons-validator:commons-validator:1.6' implementation 'de.odysseus.juel:juel-impl:2.2.7' implementation 'net.fortuna.ical4j:ical4j:1.0-rc3-atlassian-11' + implementation 'net.lingala.zip4j:zip4j:2.6.4' implementation 'org.apache.ant:ant-junit:1.10.8' implementation 'org.apache.commons:commons-collections4:4.4' implementation 'org.apache.commons:commons-dbcp2:2.7.0' + implementation 'org.apache.commons:commons-imaging:1.0-alpha2' // Alpha but OK, "Imaging was working and was used by a number of projects in production even before reaching its initial release as an Apache Commons component." implementation 'org.apache.commons:commons-text:1.8' implementation 'org.apache.geronimo.components:geronimo-transaction:3.1.4' implementation 'org.apache.geronimo.specs:geronimo-jms_1.1_spec:1.1.1' @@ -183,10 +185,12 @@ dependencies { implementation 'org.apache.poi:poi:4.1.2' implementation 'org.apache.shiro:shiro-core:1.4.1' // So far we did not update from 1.4.1 because of a compile issue. You may try w/ a newer version than 1.5.2 implementation 'org.apache.sshd:sshd-core:1.7.0' // So far we did not update from 1.7.0 because of a compile issue. You may try w/ a newer version than 2.4.0 + implementation 'org.apache.tika:tika-core:1.24.1' implementation 'org.apache.tika:tika-parsers:1.24.1' implementation 'org.apache.tomcat:tomcat-catalina-ha:9.0.37' // Remember to change the version number in javadoc block implementation 'org.apache.tomcat:tomcat-jasper:9.0.37' implementation 'org.apache.axis2:axis2-kernel:1.7.9' + implementation 'org.apache.xmlgraphics:batik:1.13' implementation 'org.apache.xmlgraphics:fop:2.3' // NOTE: in 2.4 dependencies are messed up. See https://github.com/moqui/moqui-fop/blob/master/build.gradle implementation 'org.apache.xmlrpc:xmlrpc-client:3.1.3' implementation 'org.apache.xmlrpc:xmlrpc-server:3.1.3' @@ -202,6 +206,7 @@ dependencies { testImplementation 'org.mockito:mockito-core:3.4.4' testImplementation 'org.jmockit:jmockit:1.49' testImplementation 'com.pholser:junit-quickcheck-generators:0.9.2' + runtimeOnly 'javax.xml.soap:javax.xml.soap-api:1.4.0' runtimeOnly 'de.odysseus.juel:juel-spi:2.2.7' runtimeOnly 'net.sf.barcode4j:barcode4j-fop-ext:2.1' @@ -289,7 +294,7 @@ checkstyle { // the sum of errors found last time it was changed after using the // ‘checkstyle’ tool present in the framework and in the official // plugins. - tasks.checkstyleMain.maxErrors = 502 + tasks.checkstyleMain.maxErrors = 280 // Currently there are a lot of errors so we need to temporarily // hide them to avoid polluting the terminal output. showViolations = false diff --git a/framework/base/src/main/java/org/apache/ofbiz/base/util/FileUtil.java b/framework/base/src/main/java/org/apache/ofbiz/base/util/FileUtil.java index 7d08226..068f365 100644 --- a/framework/base/src/main/java/org/apache/ofbiz/base/util/FileUtil.java +++ b/framework/base/src/main/java/org/apache/ofbiz/base/util/FileUtil.java @@ -49,6 +49,9 @@ import java.util.zip.ZipOutputStream; import org.apache.commons.io.FileUtils; import org.apache.ofbiz.base.location.ComponentLocationResolver; +import net.lingala.zip4j.ZipFile; +import net.lingala.zip4j.exception.ZipException; + /** * File Utilities */ @@ -521,4 +524,26 @@ public final class FileUtil { } } + /** + * Unzip file structure of the given zipFile to specified outputFolder The Zip slip vulnerabilty is handled since version 1.3.3 of + * net.lingala.zip4j.ZipFile; unzipFileToFolder is not as reliable and does not handle passwords + * @param source + * @param destination + * @param password optional + * @return true if OK + */ + public static boolean unZip(String source, String destination, String password) { + try { + if (password.isEmpty()) { + new ZipFile(source).extractAll(destination); + } else { + new ZipFile(source, password.toCharArray()).extractAll(destination); + } + } catch (ZipException e) { + Debug.logError("Error extracting [" + source + "] file to dir destination: " + destination, e.toString(), MODULE); + return false; + } + return true; + } + } diff --git a/framework/base/src/main/java/org/apache/ofbiz/base/util/HttpRequestFileUpload.java b/framework/base/src/main/java/org/apache/ofbiz/base/util/HttpRequestFileUpload.java index 2611188..e3734a6 100644 --- a/framework/base/src/main/java/org/apache/ofbiz/base/util/HttpRequestFileUpload.java +++ b/framework/base/src/main/java/org/apache/ofbiz/base/util/HttpRequestFileUpload.java @@ -31,6 +31,8 @@ import java.util.Map; import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServletRequest; +import org.apache.commons.imaging.ImageReadException; +import org.apache.ofbiz.entity.Delegator; /** * HttpRequestFileUpload - Receive a file upload through an HttpServletRequest @@ -147,19 +149,19 @@ public class HttpRequestFileUpload { * @param request the request * @throws IOException the io exception */ - public void doUpload(HttpServletRequest request) throws IOException { + public boolean doUpload(HttpServletRequest request, String fileType) throws IOException { + Delegator delegator = (Delegator) request.getAttribute("delegator"); ServletInputStream in = request.getInputStream(); String reqLengthString = request.getHeader("content-length"); - Debug.logInfo("expect " + reqLengthString + " bytes.", MODULE); int requestLength = 0; try { requestLength = Integer.parseInt(reqLengthString); - } catch (Exception e2) { - e2.printStackTrace(); - return; + } catch (NumberFormatException e) { + Debug.logError(e, e.getMessage(), MODULE); + return false; } byte[] line = new byte[BUFFER_SIZE]; @@ -168,7 +170,8 @@ public class HttpRequestFileUpload { i = waitingReadLine(in, line, 0, BUFFER_SIZE, requestLength); requestLength -= i; if (i < 3) { - return; + Debug.logError("Possibly a waitingReadLine error", MODULE); + return false; } int boundaryLength = i - 2; @@ -187,7 +190,7 @@ public class HttpRequestFileUpload { if (newLine.indexOf("filename=\"") != -1) { setFilename(new String(line, 0, i - 2, StandardCharsets.UTF_8)); if (filename == null) { - return; + return false; } // this is the file content i = waitingReadLine(in, line, 0, BUFFER_SIZE, requestLength); @@ -225,8 +228,8 @@ public class HttpRequestFileUpload { } } - try ( - FileOutputStream fos = new FileOutputStream(savePath + filenameToUse);) { + String fileTocheck = savePath + filenameToUse; + try (FileOutputStream fos = new FileOutputStream(fileTocheck);) { boolean bail = (new String(line, 0, i, StandardCharsets.UTF_8).startsWith(boundary)); boolean oneByteLine = (i == 1); // handle one-byte lines @@ -266,10 +269,13 @@ public class HttpRequestFileUpload { } } fos.flush(); - } catch (RuntimeException e) { - throw e; - } catch (Exception e) { + + if (!org.apache.ofbiz.security.SecuredUpload.isValidFile(fileTocheck, fileType, delegator)) { + return false; + } + } catch (ImageReadException e) { Debug.logError(e, MODULE); + return false; } } else { // this is a field @@ -293,7 +299,7 @@ public class HttpRequestFileUpload { i = waitingReadLine(in, line, 0, BUFFER_SIZE, requestLength); requestLength -= i; if ((i == boundaryLength + 2 || i == boundaryLength + 4) // + 4 is eof - && (new String(line, 0, i).startsWith(boundary))) { + && (new String(line, 0, i).startsWith(boundary))) { fieldValue.append(newLine.substring(0, newLine.length() - 2)); } else { fieldValue.append(newLine); @@ -309,6 +315,7 @@ public class HttpRequestFileUpload { } } // end while + return true; } // reads a line, waiting if there is nothing available and reqLen > 0 @@ -329,7 +336,7 @@ public class HttpRequestFileUpload { while (endMS > (new Date().getTime())) { try { wait(WAIT_INTERVAL); - } catch (Exception e3) { + } catch (InterruptedException e) { Debug.logInfo(".", MODULE); } } @@ -337,4 +344,5 @@ public class HttpRequestFileUpload { } return i; } + } diff --git a/framework/base/src/main/java/org/apache/ofbiz/base/util/template/FreeMarkerWorker.java b/framework/base/src/main/java/org/apache/ofbiz/base/util/template/FreeMarkerWorker.java index 92e7bec..b241d92 100644 --- a/framework/base/src/main/java/org/apache/ofbiz/base/util/template/FreeMarkerWorker.java +++ b/framework/base/src/main/java/org/apache/ofbiz/base/util/template/FreeMarkerWorker.java @@ -126,8 +126,7 @@ public final class FreeMarkerWorker { } catch (TemplateException e) { Debug.logError("Unable to set date/time and number formats in FreeMarker: " + e, MODULE); } - String templateClassResolver = UtilProperties.getPropertyValue("security", "templateClassResolver", - "SAFER_RESOLVER"); + String templateClassResolver = UtilProperties.getPropertyValue("security", "templateClassResolver", "SAFER_RESOLVER"); switch (templateClassResolver) { case "UNRESTRICTED_RESOLVER": newConfig.setNewBuiltinClassResolver(TemplateClassResolver.UNRESTRICTED_RESOLVER); diff --git a/framework/common/config/SecurityUiLabels.xml b/framework/common/config/SecurityUiLabels.xml index 2fef15b..5410bde 100644 --- a/framework/common/config/SecurityUiLabels.xml +++ b/framework/common/config/SecurityUiLabels.xml @@ -762,6 +762,26 @@ <value xml:lang="zh">SecurityViewPermissionError 你没有权限浏览本页面。 (需要"SECURITY_VIEW" 或 "SECURITY_ADMIN")</value> <value xml:lang="zh-TW">SecurityViewPermissionError 你沒有權限檢視本頁面. (需要"SECURITY_VIEW" 或 "SECURITY_ADMIN")</value> </property> + <property key="SupportedFileIncludingSvgFormats"> + <value xml:lang="en">For security reason only valid files of supported image formats (GIF, JPEG, PNG, TIFF), SVG, PDF, and ZIP or text files with safe contents are accepted.</value> + <value xml:lang="fr">Pour des raisons de sécurité, seuls les fichiers valides de formats d'image pris en charge (GIF, JPEG, PNG, TIFF), les fichiers SVG, PDF, et les fichiers ZIP ou texte aux contenus sûrs sont acceptés.</value> + </property> + <property key="SupportedFileFormats"> + <value xml:lang="en">For security reason only valid files of supported image formats (GIF, JPEG, PNG, TIFF), PDF or text files with safe contents are accepted.</value> + <value xml:lang="fr">Pour des raisons de sécurité, seuls les fichiers valides de formats d'image pris en charge (GIF, JPEG, PNG, TIFF), les fichiers PDF ou les fichiers texte aux contenus sûrs sont acceptés.</value> + </property> + <property key="SupportedImageFormatseIncludingSvg"> + <value xml:lang="en">For security reason only valid files of supported image formats (GIF, JPEG, PNG, TIFF), or SVG format are accepted.</value> + <value xml:lang="fr">Pour des raisons de sécurité, seuls les fichiers valides des formats d'image pris en charge (GIF, JPEG, PNG, TIFF) ou au format SVG sont acceptés.</value> + </property> + <property key="SupportedImageFormats"> + <value xml:lang="en">For security reason only valid files of supported image formats (GIF, JPEG, PNG, TIFF) are accepted.</value> + <value xml:lang="fr">Pour des raisons de sécurité, seuls les fichiers valides des formats d'image pris en charge (GIF, JPEG, PNG, TIFF) sont acceptés.</value> + </property> + <property key="SupportedTextFileFormats"> + <value xml:lang="en">For security reason only text files with safe contents are accepted.</value> + <value xml:lang="fr">Pour des raisons de sécurité, seuls les fichiers texte dont les contenus sont sûrs sont acceptés.</value> + </property> <property key="UserLogin"> <value xml:lang="de">Benutzeranmeldung</value> <value xml:lang="en">User Login</value> diff --git a/framework/security/config/security.properties b/framework/security/config/security.properties index bf1d075..c904be3 100644 --- a/framework/security/config/security.properties +++ b/framework/security/config/security.properties @@ -158,8 +158,8 @@ host-headers-allowed=localhost,127.0.0.1,demo-trunk.ofbiz.apache.org,demo-stable # -- 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 +# -- If you use 'lax' we recommend that you set +# -- org.apache.ofbiz.security.CsrfDefenseStrategy # -- for csrf.defense.strategy (see below) SameSiteCookieAttribute= @@ -182,9 +182,9 @@ csrf.entity.request.limit= # -- Because OOTB OFBiz also sets the SameSite attribute to 'strict' for all cookies, # -- which is an effective CSRF defense, # -- default is org.apache.ofbiz.security.NoCsrfDefenseStrategy if not specified. -# -- Use org.apache.ofbiz.security.CsrfDefenseStrategy +# -- Use org.apache.ofbiz.security.CsrfDefenseStrategy # -- if you need to use a 'lax' for SameSiteCookieAttribute. -# -- +# -- # -- Or if you, or your users, use, or may use, a browser version that # -- is not supporting the SameSite cookie attribute # -- You may refer to https://caniuse.com/#feat=same-site-cookie-attribute @@ -192,9 +192,38 @@ csrf.defense.strategy= # -- Freemarker TemplateClassResolver option, see OFBIZ-11709. -# -- By default OFBiz uses the SAFER_RESOLVER because OOTB it does not use any of the Freemarker classes -# -- that SAFER_RESOLVER prevents: ObjectConstructor, Execute and JythonRuntime. +# -- By default OFBiz uses the SAFER_RESOLVER because OOTB it does not use any of the Freemarker classes +# -- that SAFER_RESOLVER prevents: ObjectConstructor, Execute and JythonRuntime. # -- If you need to use one to these classes you need to change the TemplateClassResolver -# -- to UNRESTRICTED_RESOLVER and look at MemberAccessPolicy. In any cases better read +# -- to UNRESTRICTED_RESOLVER and look at MemberAccessPolicy. In any cases better read # -- https://freemarker.apache.org/docs/app_faq.html#faq_template_uploading_security templateClassResolver= + + +#-- UPLOAD: supported file formats are *safe* PNG, GIF, TIFF, JPEG, PDF and ZIP +#-- +#-- No proprietary file formats (Excel, Word, etc.) are handled OOTB. +#-- They can be handled by custom projects using https://github.com/righettod/document-upload-protection: +#-- https://github.com/OWASP/www-projectchapter-example/blob/master/cheatsheets/Protect_FileUpload_Against_Malicious_File.md +#-- Also Tika is an option, but you have to check Tika code, to be sure it's secure enough (ie don't use only metadata) +#-- +#-- Apache Commons Imaging is used for images. +#-- For supported formats see https://commons.apache.org/proper/commons-imaging/formatsupport.html +#-- Notably https://commons.apache.org/proper/commons-imaging/formatsupport.html#Metadata_Format_Support +#-- OOTB OFBiz only supports PNG, GIF, TIFF and JPEG, it's a breeze to extend using more: +#-- commonsImagingSupportedFormats=BMP,GIF,JPEG/JFIF,ICNS,ICO/CUR,PCX/DCX,PNM/PGM/PBM/PPM/PAMPortablePixmap,PNG,PSD/Photoshop,RGBE/RadianceHDR,TIFF,WBMP,XBM,XPM +#-- You should then modify SupportedImageFormats label. +#-- +#-- If you want to get more image formats then use imageJ: +#-- For imagejSupportedFormats see https://imagejdocu.tudor.lu/faq/general/which_file_formats_are_supported_by_imagej. NOTE: plugins support is important here +#-- imagejSupportedFormats=TIFF(.tiff,.tif),JPEG(.jpeg,.jpg),BMP(.bmp),FITS(.fits),PGM(.pgm),PPM(.ppm),PBM(.pbm),GIF(.gif),AnimatedGIF(.gif),PNG(.png),DICOM(.dic,.dcm,.dicom),PICT(.pict,.pic,.pct),PSD(.psd),TGA(.tga),ICO(.ico),CUR(.cur),Sunraster(.sun),XBM(.xbm),XPM(.xpm),PCX(.pcx),ANALYZE,NIfTi,AHF(.ahf),SPE(.spe),PIC(.pic),LeicaTIFF(.tiff,.lei),Quicktime(.pic,.mov),AVI(.avi),PDS(.pds),LSM(.lsm),RAW,ISAC,FluoViewTIFF(.tiff),FluoviewFV1000OIB(.oib),FluoviewFV1000OIF(.oif,.tif,-ro.pty,.lu [...] +#-- +#-- PDFBox and PDFReader are used for PDF files +#-- +#-- For text files, the philosophy is we can't presume of all possible text contents used for attacks with payloads +#-- At least there is an easy way to prevent them in SecuredUpload::isValidTextFile +#-- +#-- 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 +#-- As it name says, allowAllUploads opens all possibilities +allowAllUploads= 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 new file mode 100644 index 0000000..4650dfd --- /dev/null +++ b/framework/security/src/main/java/org/apache/ofbiz/security/SecuredUpload.java @@ -0,0 +1,548 @@ + +/******************************************************************************* + * 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.ofbiz.security; + +import java.awt.Graphics; +import java.awt.Image; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.UUID; + +import javax.imageio.ImageIO; +import javax.imageio.ImageReader; +import javax.imageio.stream.ImageInputStream; + +import org.apache.batik.dom.svg.SAXSVGDocumentFactory; +import org.apache.batik.util.XMLResourceDescriptor; +import org.apache.commons.imaging.ImageFormat; +import org.apache.commons.imaging.ImageFormats; +import org.apache.commons.imaging.ImageInfo; +import org.apache.commons.imaging.ImageParser; +import org.apache.commons.imaging.ImageReadException; +import org.apache.commons.imaging.ImageWriteException; +import org.apache.commons.imaging.Imaging; +import org.apache.commons.imaging.formats.gif.GifImageParser; +import org.apache.commons.imaging.formats.jpeg.JpegImageParser; +import org.apache.commons.imaging.formats.png.PngImageParser; +import org.apache.commons.imaging.formats.tiff.TiffImageParser; +import org.apache.commons.io.FileUtils; +import org.apache.ofbiz.base.util.Debug; +import org.apache.ofbiz.base.util.FileUtil; +import org.apache.ofbiz.entity.Delegator; +import org.apache.ofbiz.entity.util.EntityUtilProperties; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDDocumentNameDictionary; +import org.apache.pdfbox.pdmodel.PDEmbeddedFilesNameTreeNode; +import org.apache.tika.Tika; +import org.apache.tika.exception.TikaException; +import org.apache.tika.metadata.Metadata; +import org.apache.tika.metadata.TikaCoreProperties; +import org.apache.tika.parser.AutoDetectParser; +import org.apache.tika.parser.ParseContext; +import org.apache.tika.parser.Parser; +import org.apache.tika.parser.RecursiveParserWrapper; +import org.apache.tika.sax.BasicContentHandlerFactory; +import org.apache.tika.sax.ContentHandlerFactory; +import org.apache.tika.sax.RecursiveParserWrapperHandler; +import org.xml.sax.SAXException; + +import com.lowagie.text.pdf.PdfReader; + +public class SecuredUpload { + + // This can be helpful: + // https://en.wikipedia.org/wiki/File_format + // https://en.wikipedia.org/wiki/List_of_file_signatures + // See also information in security.properties: + // Line #-- UPLOAD: supported file formats are *safe* PNG, GIF, TIFF, JPEG, PDF and ZIP + + private static final String MODULE = SecuredUpload.class.getName(); + + /** + * @param fileTocheck + * @param fileType + * @return true if the file is valid + * @throws IOException + * @throws ImageReadException + */ + public static boolean isValidFile(String fileTocheck, String fileType, Delegator delegator) throws IOException, ImageReadException { + + if (("true".equalsIgnoreCase(EntityUtilProperties.getPropertyValue("security", "allowAllUploads", delegator)))) { + return true; + } + + if (isExecutable(fileTocheck)) { + return false; + } + + switch (fileType) { + case "Image": + if (isValidImageFile(fileTocheck)) { + return true; + } + break; + + case "ImageAndSvg": + if (isValidImageIncludingSvgFile(fileTocheck)) { + return true; + } + break; + + case "PDF": + if (isValidPdfFile(fileTocheck)) { + return true; + } + break; + + case "Compressed": + if (isValidCompressedFile(fileTocheck, delegator)) { + return true; + } + break; + + case "AllButCompressed": + if (isValidTextFile(fileTocheck) + || isValidImageIncludingSvgFile(fileTocheck) + || isValidPdfFile(fileTocheck)) { + return true; + } + break; + + case "Text": + // The philosophy for isValidTextFile() is that + // we can't presume of all possible text contents used for attacks with payloads + // At least there is an easy way to prevent them in isValidTextFile + if (isValidTextFile(fileTocheck)) { + return true; + } + break; + + // case "Audio": TODO if needed + // break; + // case "Video": TODO if needed + // break; + + default: // All + if (isValidTextFile(fileTocheck) + || isValidImageIncludingSvgFile(fileTocheck) + || isValidCompressedFile(fileTocheck, delegator) + || isValidPdfFile(fileTocheck)) { + return true; + } + break; + } + Debug.logError("File :" + fileTocheck + ", can't be uploaded for security reason", MODULE); + File badFile = new File(fileTocheck); + if (!badFile.delete()) { + Debug.logError("File :" + fileTocheck + ", couldn't be deleted", MODULE); + } + return false; + } + + /** + * Is it a supported image format? + * @param fileName + * @return true if it's a valid image file + * @throws IOException ImageReadException + */ + private static boolean isValidImageFile(String fileName) throws ImageReadException, IOException { + Path filePath = Paths.get(fileName); + byte[] bytesFromFile = Files.readAllBytes(filePath); + ImageFormat imageFormat = Imaging.guessFormat(bytesFromFile); + return imageFormat.equals(ImageFormats.PNG) + || imageFormat.equals(ImageFormats.GIF) + || imageFormat.equals(ImageFormats.TIFF) + || imageFormat.equals(ImageFormats.JPEG) + && imageMadeSafe(fileName); + } + + /** + * Implementation based on https://github.com/righettod/document-upload-protection sanitizer for Image file. See + * https://github.com/righettod/document-upload-protection/blob/master/src/main/java/eu/righettod/poc/sanitizer/ImageDocumentSanitizerImpl.java + * Uses Java built-in API in complement of Apache Commons Imaging for format not supported by the built-in API. See + * http://commons.apache.org/proper/commons-imaging/ and http://commons.apache.org/proper/commons-imaging/formatsupport.html + */ + private static boolean imageMadeSafe(String fileName) { + File file = new File(fileName); + boolean safeState = false; + boolean fallbackOnApacheCommonsImaging; + try { + if ((file != null) && file.exists() && file.canRead() && file.canWrite()) { + // Get the image format + String formatName; + ImageInputStream iis = ImageIO.createImageInputStream(file); + Iterator<ImageReader> imageReaderIterator = ImageIO.getImageReaders(iis); + // If there not ImageReader instance found so it's means that the current format is not supported by the Java built-in API + if (!imageReaderIterator.hasNext()) { + ImageInfo imageInfo = Imaging.getImageInfo(file); + if (imageInfo != null && imageInfo.getFormat() != null && imageInfo.getFormat().getName() != null) { + formatName = imageInfo.getFormat().getName(); + fallbackOnApacheCommonsImaging = true; + } else { + throw new IOException("Format of the original image " + fileName + " is not supported for read operation !"); + } + } else { + ImageReader reader = imageReaderIterator.next(); + formatName = reader.getFormatName(); + fallbackOnApacheCommonsImaging = false; + iis.close(); // This was not correctly handled in the document-upload-protection example, and I did not spot it :/ + } + + // Load the image + BufferedImage originalImage; + if (!fallbackOnApacheCommonsImaging) { + originalImage = ImageIO.read(file); + } else { + originalImage = Imaging.getBufferedImage(file); + } + + // Check that image has been successfully loaded + if (originalImage == null) { + throw new IOException("Cannot load the original image " + fileName + "!"); + } + + // Get current Width and Height of the image + int originalWidth = originalImage.getWidth(null); + int originalHeight = originalImage.getHeight(null); + + // Resize the image by removing 1px on Width and Height + Image resizedImage = originalImage.getScaledInstance(originalWidth - 1, originalHeight - 1, Image.SCALE_SMOOTH); + + // Resize the resized image by adding 1px on Width and Height - In fact set image to is initial size + Image initialSizedImage = resizedImage.getScaledInstance(originalWidth, originalHeight, Image.SCALE_SMOOTH); + + // Save image by overwriting the provided source file content + BufferedImage sanitizedImage = new BufferedImage(initialSizedImage.getWidth(null), initialSizedImage.getHeight(null), + BufferedImage.TYPE_INT_RGB); + 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 { + ImageParser imageParser; + // 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": { + imageParser = new TiffImageParser(); + break; + } + case "GIF": { + imageParser = new GifImageParser(); + break; + } + case "PNG": { + imageParser = new PngImageParser(); + break; + } + case "JPEG": { + imageParser = new JpegImageParser(); + break; + } + 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; + } + + /** + * Is it a supported image format, including SVG? + * @param fileName + * @return true if it's a valid image file + * @throws IOException ImageReadException + */ + private static boolean isValidImageIncludingSvgFile(String fileName) throws ImageReadException, IOException { + Path filePath = Paths.get(fileName); + byte[] bytesFromFile = Files.readAllBytes(filePath); + ImageFormat imageFormat = Imaging.guessFormat(bytesFromFile); + return imageFormat.equals(ImageFormats.PNG) + || imageFormat.equals(ImageFormats.GIF) + || imageFormat.equals(ImageFormats.TIFF) + || imageFormat.equals(ImageFormats.JPEG) + || isValidSvgFile(fileName); + } + + /** + * Is it an SVG file? + * @param fileName + * @return true if it's a valid SVG file + * @throws IOException + */ + private static boolean isValidSvgFile(String fileName) throws IOException { + Path filePath = Paths.get(fileName); + String parser = XMLResourceDescriptor.getXMLParserClassName(); + SAXSVGDocumentFactory f = new SAXSVGDocumentFactory(parser); + try { + f.createDocument(filePath.toUri().toString()); + } catch (IOException e) { + return false; + } + return isValidTextFile(fileName); + } + + /** + * @param fileName + * @return true if it's a safe PDF file: is PDF and does not contains embedded files + * @throws IOException If there is an error parsing the document + */ + private static boolean isValidPdfFile(String fileName) throws IOException { + File file = new File(fileName); + boolean safeState = false; + try { + if ((file != null) && file.exists()) { + // Load stream in PDF parser + // If the stream is not a PDF then exception will be thrown and safe state will be set to FALSE + PdfReader reader = new PdfReader(file.getAbsolutePath()); + // Check 1: detect if the document contains any JavaScript code + String jsCode = reader.getJavaScript(); + if (jsCode == null) { + // OK no JS code, pass to check 2: detect if the document has any embedded files + PDEmbeddedFilesNameTreeNode efTree = null; + try (PDDocument pdDocument = PDDocument.load(file)) { + PDDocumentNameDictionary names = new PDDocumentNameDictionary(pdDocument.getDocumentCatalog()); + efTree = names.getEmbeddedFiles(); + } + safeState = efTree == null; + } + } + } catch (Exception e) { + safeState = false; + Debug.logError(e, "for security reason the PDF file " + file.getAbsolutePath() + "can't be uploaded !", MODULE); + } + return safeState; + } + + private static boolean isExecutable(String fileName) throws IOException { + String mimeType = getMimeTypeFromFileName(fileName); + // Check for Windows executable. Neglect .bat and .ps1: https://s.apache.org/c8sim + if ("application/x-msdownload".equals(mimeType) || "application/x-ms-installer".equals(mimeType)) { + Debug.logError("The file" + fileName + " is a Windows executable, for security reason it's not accepted :", MODULE); + return true; + } + // Check for ELF (Linux) and scripts + if ("application/x-elf".equals(mimeType) + || "application/x-sh".equals(mimeType) + || "application/text/x-perl".equals(mimeType) + || "application/text/x-python".equals(mimeType)) { + Debug.logError("The file" + fileName + " is a Linux executable, for security reason it's not accepted :", MODULE); + return true; + } + return false; + } + + /** + * Check if the compressed file is valid Does not handle compressed files in sub folders of compressed files. Handles only ZIP files, if you need + * bzip, rar, tar or/and 7z file formats they can be handled by Apache commons-compress: Types based on + * https://developer.mozilla.org/fr/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types For code explanations see + * http://commons.apache.org/proper/commons-compress/examples.html + * @param fileName + * @return true if it's a valid compressed file + * @throws IOException ImageReadException + */ + private static boolean isValidCompressedFile(String fileName, Delegator delegator) throws IOException, ImageReadException { + String mimeType = getMimeTypeFromFileName(fileName); + // I planned to handle more formats but did only ZIP + // The code can be extended based on that + // if ("application/octet-stream".equals(mimeType) + // || "application/x-bzip".equals(mimeType) + // || "application/x-bzip2".equals(mimeType) + // || "application/java-archive".equals(mimeType) + // || "application/x-rar-compressed".equals(mimeType) + // || "application/x-tar".equals(mimeType) + // || "application/zip".equals(mimeType) + // || "application/x-zip-compressed".equals(mimeType) + // || "multipart/x-zip".equals(mimeType) + // || "application/x-7z-compressed".equals(mimeType)) { + + // Handles only Zip format OOTB + File fileToCheck = new File(fileName); + String folderName = fileToCheck.getParentFile().toString() + File.separator + UUID.randomUUID(); + if ("application/octet-stream".equals(mimeType) + || "application/java-archive".equals(mimeType) + || "application/zip".equals(mimeType) + || "application/x-zip-compressed".equals(mimeType) + || "multipart/x-zip".equals(mimeType)) { + if (!FileUtil.unZip(fileName, folderName, "")) { + return false; + } else { + // Keep it like that to allow to spot other file types which could be included... + // try { + // recursiveParserWrapper(fileName); + // } catch (SAXException | TikaException e) { + // // TODO Auto-generated catch block + // e.printStackTrace(); + // } + // Recursive method to check inside directories + return isValidDirectoryInCompressedFile(folderName, delegator); + } + } + return false; + } + + /* + * According to http://tika.apache.org/1.24.1/detection.html#The_default_Tika_Detector The simplest way to detect is through the Tika Facade + * class, which provides methods to detect based on File, InputStream, InputStream and Filename, Filename or a few others. It works best with a + * File or TikaInputStream. + * @param fileName + * @return true if the file is valid + */ + private static String getMimeTypeFromFileName(String fileName) throws IOException { + File file = new File(fileName); + Tika tika = new Tika(); + return tika.detect(file); + } + + private static boolean isValidDirectoryInCompressedFile(String folderName, Delegator delegator) throws IOException, ImageReadException { + File folder = new File(folderName); + Collection<File> files = FileUtils.listFiles(folder, null, true); + for (File f : files) { + if (f.isDirectory()) { + Collection<File> dirInside = FileUtils.listFiles(f, null, true); + for (File insideFile : dirInside) { + if (!isValidDirectoryInCompressedFile(insideFile.getAbsolutePath(), delegator)) { + FileUtils.deleteDirectory(folder); + return false; + } + } + } else if (!isValidFile(f.getAbsolutePath(), "AllButCompressed", delegator)) { + FileUtils.deleteDirectory(folder); + return false; + } + } + FileUtils.deleteDirectory(folder); + return true; + } + + /** + * For documents that may contain embedded documents, it might be helpful to create list of metadata objects, one for the container document and + * one for each embedded document. This allows easy access to both the extracted content and the metadata of each embedded document. Note that + * many document formats can contain embedded documents, including traditional container formats -- zip, tar and others -- but also common office + * document formats including: MSWord, MSExcel, MSPowerPoint, RTF, PDF, MSG and several others. + * <p> + * The "content" format is determined by the ContentHandlerFactory, and the content is stored in + * {@link org.apache.tika.parser.RecursiveParserWrapper#TIKA_CONTENT} + * <p> + * The drawback to the RecursiveParserWrapper is that it caches metadata and contents in memory. This should not be used on files whose contents + * are too big to be handled in memory. + * @return a list of metadata object, one each for the container file and each embedded file + * @throws IOException + * @throws SAXException + * @throws TikaException + */ + // This can turn to be useful, so I let it there... + // Inspired by https://cwiki.apache.org/confluence/display/tika/RecursiveMetadata + // And https://stackoverflow.com/questions/62132310/apache-tika-exctract-file-names-and-mime-types-from-archive + @SuppressWarnings("unused") + private static Set<String> recursiveParserWrapper(String fileName) throws IOException, SAXException, TikaException { + File file = new File(fileName); + Parser p = new AutoDetectParser(); + ContentHandlerFactory factory = new BasicContentHandlerFactory(BasicContentHandlerFactory.HANDLER_TYPE.IGNORE, -1); + RecursiveParserWrapper wrapper = new RecursiveParserWrapper(p); + Metadata metadata = new Metadata(); + metadata.set(TikaCoreProperties.ORIGINAL_RESOURCE_NAME, file.getName()); + ParseContext context = new ParseContext(); + RecursiveParserWrapperHandler handler = new RecursiveParserWrapperHandler(factory, -1); + try (InputStream stream = new FileInputStream(file)) { + wrapper.parse(stream, handler, metadata, context); + } + List<Metadata> metadatas = handler.getMetadataList(); + Set<String> mimeTypes = new HashSet<>(); + for (Metadata metadata1 : metadatas) { + mimeTypes.add(metadata1.get(Metadata.CONTENT_TYPE)); + } + return mimeTypes; + } + + /** + * Does this text file contains a Freemarker Server-Side Template Injection (SSTI) using freemarker.template.utility.Execute? Etc. + * @param fileName must be an UTF-8 encoded text file + * @return true if the text file does not contains a Freemarker SSTI + * @throws IOException + */ + private static boolean isValidTextFile(String fileName) throws IOException { + Path filePath = Paths.get(fileName); + byte[] bytesFromFile = Files.readAllBytes(filePath); + try { + Charset.availableCharsets().get("UTF-8").newDecoder().decode(ByteBuffer.wrap(bytesFromFile)); + } catch (CharacterCodingException e) { + return false; + } + String content = new String(bytesFromFile); + return !(content.toLowerCase().contains("freemarker") // Should be OK, should not be used in Freemarker templates, not part of the syntax. + // Else "template.utility.Execute" is a good replacement but not as much catching, who + // knows... + || content.toLowerCase().contains("import=\"java") + || content.toLowerCase().contains("Runtime.getRuntime().exec(") + || content.toLowerCase().contains("<%@ page") + || content.toLowerCase().contains("<script") + || content.toLowerCase().contains("<body>") + || content.toLowerCase().contains("<form") + || content.toLowerCase().contains("php") + || content.toLowerCase().contains("javascript") + || content.toLowerCase().contains("%eval") + || content.toLowerCase().contains("@eval") + || content.toLowerCase().contains("import os") // Python + || content.toLowerCase().contains("passthru") + || content.toLowerCase().contains("exec") + || content.toLowerCase().contains("shell_exec") + || content.toLowerCase().contains("assert") + || content.toLowerCase().contains("str_rot13") + || content.toLowerCase().contains("system") + || content.toLowerCase().contains("phpinfo") + || content.toLowerCase().contains("base64_decode") + || content.toLowerCase().contains("chmod") + || content.toLowerCase().contains("mkdir") + || content.toLowerCase().contains("fopen") + || content.toLowerCase().contains("fclose") + || content.toLowerCase().contains("readfile")); + // TODO.... to be continued with known webshell contents... a complete allow list is impossible anyway + // eg: https://www.acunetix.com/blog/articles/detection-prevention-introduction-web-shells-part-5/ + } +} |
Free forum by Nabble | Edit this page |