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 2a33775197c..25993c5976a 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 @@ -28,6 +28,7 @@ import java.nio.ByteBuffer; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import java.nio.file.StandardOpenOption; import java.util.HashMap; @@ -92,6 +93,14 @@ public static Map addMultipleuploadForProduct(DispatchContext dc "image.management.path", delegator), context); String imageServerUrl = FlexibleStringExpander.expandString(EntityUtilProperties.getPropertyValue("catalog", "image.management.url", delegator), context); + // Guard against path traversal via productId + Path imageServerNormalizedPath = Paths.get(imageServerPath).normalize(); + Path resolvedProductDir = Paths.get(imageServerPath, productId).normalize(); + if (!resolvedProductDir.startsWith(imageServerNormalizedPath)) { + Debug.logError("Path traversal attempt detected in image management upload, productId: " + productId, MODULE); + return ServiceUtil.returnError(UtilProperties.getMessage(RES_ERROR, + "ProductImageViewUnableWriteFile", UtilMisc.toMap("fileName", resolvedProductDir.toString()), locale)); + } String rootTargetDirectory = imageServerPath; File rootTargetDir = new File(rootTargetDirectory); if (!rootTargetDir.exists()) { @@ -306,11 +315,20 @@ public static Map removeImageFileForImageManagement(DispatchCont String contentId = (String) context.get("contentId"); String dataResourceName = (String) context.get("dataResourceName"); Delegator delegator = dctx.getDelegator(); + Locale locale = (Locale) context.get("locale"); try { if (UtilValidate.isNotEmpty(contentId)) { String imageServerPath = FlexibleStringExpander.expandString(EntityUtilProperties.getPropertyValue("catalog", "image.management.path", delegator), context); + // Guard against path traversal via productId or dataResourceName + Path imageServerNormalizedPath = Paths.get(imageServerPath).normalize(); + Path resolvedFilePath = Paths.get(imageServerPath, productId, dataResourceName).normalize(); + if (!resolvedFilePath.startsWith(imageServerNormalizedPath)) { + Debug.logError("Path traversal attempt detected in image management remove, productId: " + productId, MODULE); + return ServiceUtil.returnError(UtilProperties.getMessage(RES_ERROR, + "ProductImageViewUnableWriteFile", UtilMisc.toMap("fileName", resolvedFilePath.toString()), locale)); + } File file = new File(imageServerPath + "/" + productId + "/" + dataResourceName); if (!file.delete()) { Debug.logError("File :" + file.getName() + ", couldn't be deleted", MODULE); @@ -369,7 +387,16 @@ private static Map scaleImageMangementInAllSize(DispatchContext "image.management.path", dctx.getDelegator()), context); String imageServerUrl = FlexibleStringExpander.expandString(EntityUtilProperties.getPropertyValue("catalog", "image.management.url", dctx.getDelegator()), context); - + // Guard against path traversal via productId + Path imageServerNormalizedPath = Paths.get(imageServerPath).normalize(); + Path resolvedProductDir = Paths.get(imageServerPath, productId).normalize(); + if (!resolvedProductDir.startsWith(imageServerNormalizedPath)) { + Debug.logError("Path traversal attempt detected in image management scale, productId: " + productId, MODULE); + String errMsg = UtilProperties.getMessage(RES_ERROR, + "ProductImageViewUnableWriteFile", UtilMisc.toMap("fileName", resolvedProductDir.toString()), locale); + result.put(ModelService.ERROR_MESSAGE, errMsg); + return result; + } /* get original BUFFERED IMAGE */ resultBufImgMap.putAll(ImageTransform.getBufferedImage(imageServerPath + "/" + productId + "/" + filenameToUse, locale)); @@ -536,6 +563,14 @@ public static Map createContentThumbnail(DispatchContext dctx, M "image.management.path", delegator), context); String nameOfThumb = FlexibleStringExpander.expandString(EntityUtilProperties.getPropertyValue("catalog", "image.management.nameofthumbnail", delegator), context); + // Guard against path traversal via productId + Path imageServerNormalizedPath = Paths.get(imageServerPath).normalize(); + Path resolvedProductDir = Paths.get(imageServerPath, productId).normalize(); + if (!resolvedProductDir.startsWith(imageServerNormalizedPath)) { + Debug.logError("Path traversal attempt detected in image management thumbnail, productId: " + productId, MODULE); + return ServiceUtil.returnError(UtilProperties.getMessage(RES_ERROR, + "ProductImageViewUnableWriteFile", UtilMisc.toMap("fileName", resolvedProductDir.toString()), locale)); + } // Create content for thumbnail Map contentThumb = new HashMap<>(); @@ -729,6 +764,14 @@ public static Map createNewImageThumbnail(DispatchContext dctx, String contentId = (String) context.get("contentId"); String dataResourceName = (String) context.get("dataResourceName"); String width = (String) context.get("sizeWidth"); + // Guard against path traversal via productId + Path imageServerNormalizedPath = Paths.get(imageServerPath).normalize(); + Path resolvedProductDir = Paths.get(imageServerPath, productId).normalize(); + if (!resolvedProductDir.startsWith(imageServerNormalizedPath)) { + Debug.logError("Path traversal attempt detected in create new image thumbnail, productId: " + productId, MODULE); + return ServiceUtil.returnError(UtilProperties.getMessage(RES_ERROR, + "ProductImageViewUnableWriteFile", UtilMisc.toMap("fileName", resolvedProductDir.toString()), locale)); + } String imageType = ".jpg"; int resizeWidth = Integer.parseInt(width); int resizeHeight = resizeWidth; @@ -801,6 +844,14 @@ public static Map resizeImageOfProduct(DispatchContext dctx, Map String width = (String) context.get("resizeWidth"); int resizeWidth = Integer.parseInt(width); int resizeHeight = resizeWidth; + // Guard against path traversal via productId + Path imageServerNormalizedPath = Paths.get(imageServerPath).normalize(); + Path resolvedProductDir = Paths.get(imageServerPath, productId).normalize(); + if (!resolvedProductDir.startsWith(imageServerNormalizedPath)) { + Debug.logError("Path traversal attempt detected in resize image, productId: " + productId, MODULE); + return ServiceUtil.returnError(UtilProperties.getMessage(RES_ERROR, + "ProductImageViewUnableWriteFile", UtilMisc.toMap("fileName", resolvedProductDir.toString()), locale)); + } try { BufferedImage bufImg = ImageIO.read(new File(imageServerPath + "/" + productId + "/" + dataResourceName)); @@ -831,6 +882,14 @@ public static Map renameImage(DispatchContext dctx, Map= bytes.length) { + Debug.logError("================== Not saved for security reason, JPEG missing EOI ==================", MODULE); + return false; + } + int marker = bytes[pos++] & 0xFF; + if (marker == 0xD9) { + // EOI — reject any trailing bytes + if (pos != bytes.length) { + Debug.logError("================ Not saved for security reason, JPEG has trailing bytes after EOI ================", MODULE); + return false; + } + return true; + } else if (marker >= 0xD0 && marker <= 0xD8) { + // SOI (0xD8) and RST0–RST7 (0xD0–0xD7) — no length field + continue; + } else if (marker == 0xDA) { + // SOS: length-prefixed header followed by entropy-coded scan data + if (pos + 2 > bytes.length) return false; + int len = ((bytes[pos] & 0xFF) << 8) | (bytes[pos + 1] & 0xFF); + if (len < 2 || pos + len > bytes.length) return false; + pos += len; // Skip SOS header + // Scan entropy-coded data, respecting byte stuffing (FF 00) and restart markers + while (pos < bytes.length - 1) { + if ((bytes[pos] & 0xFF) == 0xFF) { + int next = bytes[pos + 1] & 0xFF; + if (next == 0x00 || (next >= 0xD0 && next <= 0xD7)) { + pos += 2; // Stuffed 0xFF or RST — part of scan data + } else { + break; // Real marker — stop scanning scan data + } + } else { + pos++; + } + } + } else { + // Regular length-delimited segment + if (pos + 2 > bytes.length) return false; + int len = ((bytes[pos] & 0xFF) << 8) | (bytes[pos + 1] & 0xFF); + if (len < 2 || pos + len > bytes.length) return false; + pos += len; + } + } + Debug.logError("================== Not saved for security reason, JPEG missing EOI ==================", MODULE); + return false; + } catch (IOException error) { + Debug.logError("================== Not saved for security reason ==================" + error, MODULE); + return false; + } + } + + private static boolean noWebshellInGIF(File file) { + try { + byte[] bytes = Files.readAllBytes(file.toPath()); + if (!Imaging.guessFormat(bytes).equals(ImageFormats.GIF)) { + return true; // Not a GIF file, it's OK so far + } + // Header: "GIF87a" or "GIF89a" + if (bytes.length < 13) return false; + String gifHeader = new String(bytes, 0, 6, StandardCharsets.US_ASCII); + if (!"GIF87a".equals(gifHeader) && !"GIF89a".equals(gifHeader)) { + Debug.logError("================== Not saved for security reason, malformed GIF ==================", MODULE); + return false; + } + int pos = 6; + // Logical Screen Descriptor: packed byte at offset 4 within LSD + int packed = bytes[pos + 4] & 0xFF; + boolean hasGCT = (packed & 0x80) != 0; + int gctSize = packed & 0x07; + pos += 7; + // Skip Global Color Table + if (hasGCT) { + int gctBytes = 3 * (1 << (gctSize + 1)); + if (pos + gctBytes > bytes.length) return false; + pos += gctBytes; + } + // Parse blocks until Trailer + while (pos < bytes.length) { + int blockType = bytes[pos++] & 0xFF; + if (blockType == 0x3B) { + // Trailer — reject any trailing bytes + if (pos != bytes.length) { + Debug.logError("=============== Not saved for security reason, GIF has trailing bytes after Trailer ===============", MODULE); + return false; + } + return true; + } else if (blockType == 0x21) { + // Extension: label byte + sub-blocks + if (pos >= bytes.length) return false; + pos++; // Skip extension label + pos = skipGIFSubBlocks(bytes, pos); + if (pos < 0) return false; + } else if (blockType == 0x2C) { + // Image Descriptor: 9 bytes + if (pos + 9 > bytes.length) return false; + int imagePacked = bytes[pos + 8] & 0xFF; + boolean hasLCT = (imagePacked & 0x80) != 0; + int lctSize = imagePacked & 0x07; + pos += 9; + if (hasLCT) { + int lctBytes = 3 * (1 << (lctSize + 1)); + if (pos + lctBytes > bytes.length) return false; + pos += lctBytes; + } + pos++; // LZW minimum code size + pos = skipGIFSubBlocks(bytes, pos); + if (pos < 0) return false; + } else { + Debug.logError("================== Not saved for security reason, unknown GIF block type ==================", MODULE); + return false; + } + } + Debug.logError("================== Not saved for security reason, GIF missing Trailer ==================", MODULE); + return false; + } catch (IOException error) { + Debug.logError("================== Not saved for security reason ==================" + error, MODULE); + return false; + } + } + + private static int skipGIFSubBlocks(byte[] bytes, int pos) { + while (pos < bytes.length) { + int blockSize = bytes[pos++] & 0xFF; + if (blockSize == 0) { + return pos; // Block Terminator + } + pos += blockSize; + if (pos > bytes.length) return -1; + } + return -1; // Reached EOF without Block Terminator + } + private static boolean isPNG(File file) throws IOException { Path filePath = Paths.get(file.getPath()); byte[] bytesFromFile = Files.readAllBytes(filePath);