|
23 | 23 | import java.awt.Image;
|
24 | 24 | import java.awt.Transparency;
|
25 | 25 | import java.awt.image.BufferedImage;
|
| 26 | +import java.io.DataInputStream; |
26 | 27 | import java.io.File;
|
27 | 28 | import java.io.FileInputStream;
|
28 | 29 | import java.io.IOException;
|
|
39 | 40 | import java.nio.file.Paths;
|
40 | 41 | import java.nio.file.StandardOpenOption;
|
41 | 42 | import java.util.ArrayList;
|
| 43 | +import java.util.Arrays; |
| 44 | +import java.util.Base64; |
42 | 45 | import java.util.Collection;
|
| 46 | +import java.util.Collections; |
43 | 47 | import java.util.HashSet;
|
44 | 48 | import java.util.Iterator;
|
45 | 49 | import java.util.List;
|
46 | 50 | import java.util.Objects;
|
47 | 51 | import java.util.Set;
|
48 | 52 | import java.util.UUID;
|
| 53 | +import java.util.zip.DataFormatException; |
| 54 | +import java.util.zip.Inflater; |
49 | 55 |
|
50 | 56 | import javax.imageio.ImageIO;
|
51 | 57 | import javax.imageio.ImageReader;
|
|
96 | 102 | import org.w3c.dom.Document;
|
97 | 103 | import org.xml.sax.SAXException;
|
98 | 104 |
|
| 105 | +import com.drew.imaging.ImageMetadataReader; |
| 106 | +import com.drew.imaging.ImageProcessingException; |
| 107 | +import com.drew.metadata.Directory; |
| 108 | +import com.drew.metadata.Tag; |
99 | 109 | import com.lowagie.text.pdf.PdfReader;
|
100 | 110 |
|
101 | 111 | public class SecuredUpload {
|
@@ -255,7 +265,7 @@ public static boolean isValidAllFile(String fileToCheck, Delegator delegator) th
|
255 | 265 | * @throws ImageReadException
|
256 | 266 | */
|
257 | 267 | public static boolean isValidFile(String fileToCheck, String fileType, Delegator delegator) throws IOException, ImageReadException {
|
258 |
| - // Allow all |
| 268 | + // Allow all uploads w/o check |
259 | 269 | if (("true".equalsIgnoreCase(EntityUtilProperties.getPropertyValue("security", "allowAllUploads", delegator)))) {
|
260 | 270 | return true;
|
261 | 271 | }
|
@@ -398,9 +408,17 @@ && imageMadeSafe(fileName)
|
398 | 408 | */
|
399 | 409 | private static boolean imageMadeSafe(String fileName) {
|
400 | 410 | File file = new File(fileName);
|
401 |
| - boolean safeState = false; |
402 | 411 | boolean fallbackOnApacheCommonsImaging;
|
403 | 412 |
|
| 413 | + if (!noWebshellInMetadata(file)) { |
| 414 | + return false; |
| 415 | + } |
| 416 | + if (!noWebshellInPNG(file)) { |
| 417 | + return false; |
| 418 | + } |
| 419 | + |
| 420 | + boolean safeState = false; |
| 421 | + |
404 | 422 | if ((file != null) && file.exists() && file.canRead() && file.canWrite()) {
|
405 | 423 | try (OutputStream fos = Files.newOutputStream(file.toPath(), StandardOpenOption.WRITE)) {
|
406 | 424 | // Get the image format
|
@@ -486,6 +504,114 @@ private static boolean imageMadeSafe(String fileName) {
|
486 | 504 | return safeState;
|
487 | 505 | }
|
488 | 506 |
|
| 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 | + |
489 | 615 | /**
|
490 | 616 | * Is it a supported image format, including SVG?
|
491 | 617 | * @param fileName
|
@@ -840,8 +966,8 @@ private static boolean isValidVideoFile(String fileName) throws IOException {
|
840 | 966 | /**
|
841 | 967 | * Does this text file contains a Freemarker Server-Side Template Injection (SSTI) using freemarker.template.utility.Execute? Etc.
|
842 | 968 | * @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 |
845 | 971 | * @throws IOException
|
846 | 972 | */
|
847 | 973 | private static boolean isValidTextFile(String fileName, Boolean encodedContent) throws IOException {
|
|
0 commit comments