Skip to content

Commit fd6a3b8

Browse files
committed
Improved: Improve ImageManagementServices code (OFBIZ-13292)
Better handling of PNG files
1 parent cfee306 commit fd6a3b8

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.zxing:core:3.5.3'
2223
implementation 'com.googlecode.concurrentlinkedhashmap:concurrentlinkedhashmap-lru:1.4.2'

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 {
@@ -244,7 +254,7 @@ public static boolean isValidFileName(String fileToCheck, Delegator delegator) t
244254
* @throws ImageReadException
245255
*/
246256
public static boolean isValidFile(String fileToCheck, String fileType, Delegator delegator) throws IOException, ImageReadException {
247-
// Allow all
257+
// Allow all uploads w/o check
248258
if (("true".equalsIgnoreCase(EntityUtilProperties.getPropertyValue("security", "allowAllUploads", delegator)))) {
249259
return true;
250260
}
@@ -387,9 +397,17 @@ && imageMadeSafe(fileName)
387397
*/
388398
private static boolean imageMadeSafe(String fileName) {
389399
File file = new File(fileName);
390-
boolean safeState = false;
391400
boolean fallbackOnApacheCommonsImaging;
392401

402+
if (!noWebshellInMetadata(file)) {
403+
return false;
404+
}
405+
if (!noWebshellInPNG(file)) {
406+
return false;
407+
}
408+
409+
boolean safeState = false;
410+
393411
if ((file != null) && file.exists() && file.canRead() && file.canWrite()) {
394412
try (OutputStream fos = Files.newOutputStream(file.toPath(), StandardOpenOption.WRITE)) {
395413
// Get the image format
@@ -475,6 +493,114 @@ private static boolean imageMadeSafe(String fileName) {
475493
return safeState;
476494
}
477495

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

0 commit comments

Comments
 (0)