|
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 { |
@@ -244,7 +254,7 @@ public static boolean isValidFileName(String fileToCheck, Delegator delegator) t |
244 | 254 | * @throws ImageReadException |
245 | 255 | */ |
246 | 256 | public static boolean isValidFile(String fileToCheck, String fileType, Delegator delegator) throws IOException, ImageReadException { |
247 | | - // Allow all |
| 257 | + // Allow all uploads w/o check |
248 | 258 | if (("true".equalsIgnoreCase(EntityUtilProperties.getPropertyValue("security", "allowAllUploads", delegator)))) { |
249 | 259 | return true; |
250 | 260 | } |
@@ -387,9 +397,17 @@ && imageMadeSafe(fileName) |
387 | 397 | */ |
388 | 398 | private static boolean imageMadeSafe(String fileName) { |
389 | 399 | File file = new File(fileName); |
390 | | - boolean safeState = false; |
391 | 400 | boolean fallbackOnApacheCommonsImaging; |
392 | 401 |
|
| 402 | + if (!noWebshellInMetadata(file)) { |
| 403 | + return false; |
| 404 | + } |
| 405 | + if (!noWebshellInPNG(file)) { |
| 406 | + return false; |
| 407 | + } |
| 408 | + |
| 409 | + boolean safeState = false; |
| 410 | + |
393 | 411 | if ((file != null) && file.exists() && file.canRead() && file.canWrite()) { |
394 | 412 | try (OutputStream fos = Files.newOutputStream(file.toPath(), StandardOpenOption.WRITE)) { |
395 | 413 | // Get the image format |
@@ -475,6 +493,114 @@ private static boolean imageMadeSafe(String fileName) { |
475 | 493 | return safeState; |
476 | 494 | } |
477 | 495 |
|
| 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 | + |
478 | 604 | /** |
479 | 605 | * Is it a supported image format, including SVG? |
480 | 606 | * @param fileName |
@@ -826,8 +952,8 @@ private static boolean isValidVideoFile(String fileName) throws IOException { |
826 | 952 | /** |
827 | 953 | * Does this text file contains a Freemarker Server-Side Template Injection (SSTI) using freemarker.template.utility.Execute? Etc. |
828 | 954 | * @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 |
831 | 957 | * @throws IOException |
832 | 958 | */ |
833 | 959 | private static boolean isValidTextFile(String fileName, Boolean encodedContent) throws IOException { |
|
0 commit comments