|
3 | 3 |
|
4 | 4 | import org.apache.commons.csv.CSVFormat;
|
5 | 5 | import org.apache.commons.csv.CSVRecord;
|
| 6 | +import org.apache.commons.imaging.ImageInfo; |
| 7 | +import org.apache.commons.imaging.Imaging; |
| 8 | +import org.apache.commons.imaging.common.ImageMetadata; |
6 | 9 | import org.apache.commons.validator.routines.InetAddressValidator;
|
7 | 10 | import org.apache.pdfbox.Loader;
|
8 | 11 | import org.apache.pdfbox.pdmodel.PDDocument;
|
|
36 | 39 | import javax.xml.parsers.DocumentBuilder;
|
37 | 40 | import javax.xml.parsers.DocumentBuilderFactory;
|
38 | 41 | import javax.xml.parsers.ParserConfigurationException;
|
| 42 | +import java.awt.image.BufferedImage; |
39 | 43 | import java.io.*;
|
40 | 44 | import java.net.Inet6Address;
|
41 | 45 | import java.net.InetAddress;
|
@@ -849,4 +853,63 @@ public static boolean isJSONSafe(String json, int maxItemsByArraysCount, int max
|
849 | 853 | }
|
850 | 854 | return isSafe;
|
851 | 855 | }
|
| 856 | + |
| 857 | + /** |
| 858 | + * Apply a collection of validations on a image file provided:<br> |
| 859 | + * - Real image file.<br> |
| 860 | + * - Its mime type is into the list of allowed mime types.<br> |
| 861 | + * - Its metadata fields do not contains any characters related to a malicious payloads.<br> |
| 862 | + * |
| 863 | + * <br><br> |
| 864 | + * <b>Important note:</b>This implementation is prone to bypass using the "<b>raw insertion</b>" method documented in the <a href="https://www.synacktiv.com/en/publications/persistent-php-payloads-in-pngs-how-to-inject-php-code-in-an-image-and-keep-it-there">blog post</a> from the Synacktiv team.<br> |
| 865 | + * To handle such case, it is recommended to resize the image to remove any non image-related content, see <a href="https://github.com/righettod/document-upload-protection/blob/master/src/main/java/eu/righettod/poc/sanitizer/ImageDocumentSanitizerImpl.java#L54">here</a> for an example.<br> |
| 866 | + * |
| 867 | + * @param imageFilePath Filename of the image file to check. |
| 868 | + * @param imageAllowedMimeTypes List of image mime types allowed. |
| 869 | + * @return True only if the file pass all validations. |
| 870 | + * @see "https://commons.apache.org/proper/commons-imaging/" |
| 871 | + * @see "https://commons.apache.org/proper/commons-imaging/formatsupport.html" |
| 872 | + * @see "https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types" |
| 873 | + * @see "https://www.iana.org/assignments/media-types/media-types.xhtml#image" |
| 874 | + * @see "https://www.synacktiv.com/en/publications/persistent-php-payloads-in-pngs-how-to-inject-php-code-in-an-image-and-keep-it-there" |
| 875 | + * @see "https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html" |
| 876 | + * @see "https://github.com/righettod/document-upload-protection/blob/master/src/main/java/eu/righettod/poc/sanitizer/ImageDocumentSanitizerImpl.java" |
| 877 | + * @see "https://exiftool.org/examples.html" |
| 878 | + * @see "https://en.wikipedia.org/wiki/List_of_file_signatures" |
| 879 | + * @see "https://hexed.it/" |
| 880 | + * @see "https://github.com/sighook/pixload" |
| 881 | + */ |
| 882 | + public static boolean isImageSafe(String imageFilePath, List<String> imageAllowedMimeTypes) { |
| 883 | + boolean isSafe = false; |
| 884 | + Pattern payloadDetectionRegex = Pattern.compile("[<>${}`]+", Pattern.CASE_INSENSITIVE); |
| 885 | + try { |
| 886 | + File imgFile = new File(imageFilePath); |
| 887 | + if (imgFile.exists() && imgFile.canRead() && imgFile.isFile() && !imageAllowedMimeTypes.isEmpty()) { |
| 888 | + final byte[] imgBytes = Files.readAllBytes(imgFile.toPath()); |
| 889 | + //Step 1: Check the mime type of the file against the allowed ones |
| 890 | + ImageInfo imgInfo = Imaging.getImageInfo(imgBytes); |
| 891 | + if (imageAllowedMimeTypes.contains(imgInfo.getMimeType())) { |
| 892 | + //Step 2: Load the image into an object using the Image API |
| 893 | + BufferedImage imgObject = Imaging.getBufferedImage(imgBytes); |
| 894 | + if (imgObject != null && imgObject.getWidth() > 0 && imgObject.getHeight() > 0) { |
| 895 | + //Step 3: Check the metadata if the image format support it - Highly experimental |
| 896 | + List<String> metadataWithPayloads = new ArrayList<>(); |
| 897 | + final ImageMetadata imgMetadata = Imaging.getMetadata(imgBytes); |
| 898 | + if (imgMetadata != null) { |
| 899 | + imgMetadata.getItems().forEach(item -> { |
| 900 | + String metadata = item.toString(); |
| 901 | + if (payloadDetectionRegex.matcher(metadata).find()) { |
| 902 | + metadataWithPayloads.add(metadata); |
| 903 | + } |
| 904 | + }); |
| 905 | + } |
| 906 | + isSafe = metadataWithPayloads.isEmpty(); |
| 907 | + } |
| 908 | + } |
| 909 | + } |
| 910 | + } catch (Exception e) { |
| 911 | + isSafe = false; |
| 912 | + } |
| 913 | + return isSafe; |
| 914 | + } |
852 | 915 | }
|
0 commit comments