Skip to content

Commit 69cf43d

Browse files
committed
Improved: Improve ImageManagementServices code (OFBIZ-13292)
Better handling of PNG files
1 parent 9cc4fe1 commit 69cf43d

File tree

2 files changed

+131
-4
lines changed

2 files changed

+131
-4
lines changed

dependencies.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
* under the License.
1818
*/
1919
dependencies {
20+
implementation 'com.drewnoakes:metadata-extractor:2.19.0'
2021
implementation 'com.github.ben-manes.caffeine:caffeine:3.1.8'
2122
implementation 'com.google.guava:guava:33.3.1-jre'
2223
implementation 'com.google.zxing:core:3.5.3'

framework/security/src/main/java/org/apache/ofbiz/security/SecuredUpload.java

Lines changed: 130 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import java.awt.Image;
2424
import java.awt.Transparency;
2525
import java.awt.image.BufferedImage;
26+
import java.io.DataInputStream;
2627
import java.io.File;
2728
import java.io.FileInputStream;
2829
import java.io.IOException;
@@ -39,13 +40,18 @@
3940
import java.nio.file.Paths;
4041
import java.nio.file.StandardOpenOption;
4142
import java.util.ArrayList;
43+
import java.util.Arrays;
44+
import java.util.Base64;
4245
import java.util.Collection;
46+
import java.util.Collections;
4347
import java.util.HashSet;
4448
import java.util.Iterator;
4549
import java.util.List;
4650
import java.util.Objects;
4751
import java.util.Set;
4852
import java.util.UUID;
53+
import java.util.zip.DataFormatException;
54+
import java.util.zip.Inflater;
4955

5056
import javax.imageio.ImageIO;
5157
import javax.imageio.ImageReader;
@@ -96,6 +102,10 @@
96102
import org.w3c.dom.Document;
97103
import org.xml.sax.SAXException;
98104

105+
import com.drew.imaging.ImageMetadataReader;
106+
import com.drew.imaging.ImageProcessingException;
107+
import com.drew.metadata.Directory;
108+
import com.drew.metadata.Tag;
99109
import com.lowagie.text.pdf.PdfReader;
100110

101111
public class SecuredUpload {
@@ -255,7 +265,7 @@ public static boolean isValidAllFile(String fileToCheck, Delegator delegator) th
255265
* @throws ImageReadException
256266
*/
257267
public static boolean isValidFile(String fileToCheck, String fileType, Delegator delegator) throws IOException, ImageReadException {
258-
// Allow all
268+
// Allow all uploads w/o check
259269
if (("true".equalsIgnoreCase(EntityUtilProperties.getPropertyValue("security", "allowAllUploads", delegator)))) {
260270
return true;
261271
}
@@ -398,9 +408,17 @@ && imageMadeSafe(fileName)
398408
*/
399409
private static boolean imageMadeSafe(String fileName) {
400410
File file = new File(fileName);
401-
boolean safeState = false;
402411
boolean fallbackOnApacheCommonsImaging;
403412

413+
if (!noWebshellInMetadata(file)) {
414+
return false;
415+
}
416+
if (!noWebshellInPNG(file)) {
417+
return false;
418+
}
419+
420+
boolean safeState = false;
421+
404422
if ((file != null) && file.exists() && file.canRead() && file.canWrite()) {
405423
try (OutputStream fos = Files.newOutputStream(file.toPath(), StandardOpenOption.WRITE)) {
406424
// Get the image format
@@ -486,6 +504,114 @@ private static boolean imageMadeSafe(String fileName) {
486504
return safeState;
487505
}
488506

507+
private static boolean noWebshellInMetadata(File file) {
508+
com.drew.metadata.Metadata metadata = null;
509+
try {
510+
metadata = ImageMetadataReader.readMetadata(file);
511+
} catch (ImageProcessingException | IOException error) {
512+
Debug.logError("================== Not saved for security reason ==================" + error, MODULE);
513+
}
514+
515+
for (Directory directory : metadata.getDirectories()) {
516+
for (Tag tag : directory.getTags()) {
517+
try {
518+
if (!isValidText(tag.toString(), Collections.emptyList())) {
519+
Debug.logError("================== Not saved for security reason ==================", MODULE);
520+
return false;
521+
}
522+
} catch (IOException error) {
523+
Debug.logError("================== Not saved for security reason ==================" + error, MODULE);
524+
return false;
525+
}
526+
}
527+
for (String error : directory.getErrors()) {
528+
Debug.logError("================== Not saved for security reason ==================" + error, MODULE);
529+
return false;
530+
}
531+
}
532+
return true;
533+
}
534+
535+
private static boolean noWebshellInPNG(File file) {
536+
try {
537+
ImageIO.read(file);
538+
if (!isPNG(file)) {
539+
return true; // Not a PNG file, it's OK so far
540+
}
541+
} catch (IOException error) {
542+
Debug.logError("================== Not saved for security reason ==================" + error, MODULE);
543+
return false;
544+
}
545+
546+
try (DataInputStream stream = new DataInputStream(new FileInputStream(file));) {
547+
byte[] data = new byte[8];
548+
stream.readFully(data); //Read PNG Header
549+
while (true) {
550+
data = new byte[4];
551+
stream.readFully(data); //Read Length
552+
int length = ((data[0] & 0xFF) << 24)
553+
| ((data[1] & 0xFF) << 16)
554+
| ((data[2] & 0xFF) << 8)
555+
| (data[3] & 0xFF); //Byte array to int
556+
stream.readFully(data); //Read Name
557+
String name = new String(data); //Byte array to String
558+
if (name.equals("IDAT")) {
559+
data = new byte[length];
560+
stream.readFully(data); //Read Data
561+
return inflate(data);
562+
} else { //Don't care about other chunks
563+
data = new byte[length + 4]; //Data length + 4 byte CRC
564+
stream.readFully(data); //Skip Data and CRC.
565+
}
566+
}
567+
} catch (IOException error) {
568+
Debug.logError("================== Not saved for security reason, wrong PNG IDAT (weird) ==================" + error, MODULE);
569+
return false;
570+
}
571+
}
572+
573+
private static boolean isPNG(File file) throws IOException {
574+
Path filePath = Paths.get(file.getPath());
575+
byte[] bytesFromFile = Files.readAllBytes(filePath);
576+
ImageFormat imageFormat = Imaging.guessFormat(bytesFromFile);
577+
return (imageFormat.equals(ImageFormats.PNG));
578+
}
579+
580+
private static boolean inflate(byte[] data) {
581+
Inflater inflater = new Inflater();
582+
inflater.setInput(data);
583+
byte[] result = new byte[data.length * 5]; // Inflating ratio max is 5/1
584+
try {
585+
while (!inflater.finished()) {
586+
int count = inflater.inflate(result);
587+
if (count == 0) {
588+
if (!inflater.needsInput()) { // Not everything read
589+
inflater.inflate(result);
590+
} else if (inflater.needsDictionary()) { // Dictionary to be loaded
591+
inflater.setDictionary(result);
592+
inflater.getAdler();
593+
}
594+
}
595+
}
596+
if (inflater.getRemaining() > 0) { // There is more than image data in IDAT, check it
597+
byte[] remaining = Arrays.copyOfRange(data, (int) inflater.getBytesRead(), (int) inflater.getBytesRead() + inflater.getRemaining());
598+
String toCheck = new String(remaining, "UTF-8");
599+
byte[] decoded = Base64.getDecoder().decode(toCheck);
600+
String decodedStr = new String(decoded, StandardCharsets.UTF_8);
601+
if (!isValidText(decodedStr, Collections.emptyList())) {
602+
Debug.logError("================== Not saved for security reason ==================", MODULE);
603+
inflater.end();
604+
return false;
605+
}
606+
}
607+
} catch (DataFormatException | IOException error) {
608+
Debug.logError("================== Not saved for security reason ==================" + error, MODULE);
609+
inflater.end();
610+
return false;
611+
}
612+
return true;
613+
}
614+
489615
/**
490616
* Is it a supported image format, including SVG?
491617
* @param fileName
@@ -840,8 +966,8 @@ private static boolean isValidVideoFile(String fileName) throws IOException {
840966
/**
841967
* Does this text file contains a Freemarker Server-Side Template Injection (SSTI) using freemarker.template.utility.Execute? Etc.
842968
* @param fileName must be an UTF-8 encoded text file
843-
* @param encodedContent TODO
844-
* @return true if the text file does not contains a Freemarker SSTI
969+
* @param encodedContent true id the file content is encoded
970+
* @return true if the text file does not contains a Freemarker SSTI or other issues
845971
* @throws IOException
846972
*/
847973
private static boolean isValidTextFile(String fileName, Boolean encodedContent) throws IOException {

0 commit comments

Comments
 (0)