| |
| /******************************************************************************* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| *******************************************************************************/ |
| package org.apache.ofbiz.security; |
| |
| import java.awt.Graphics; |
| import java.awt.Image; |
| import java.awt.image.BufferedImage; |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStream; |
| import java.nio.ByteBuffer; |
| import java.nio.charset.CharacterCodingException; |
| import java.nio.charset.Charset; |
| import java.nio.file.Files; |
| import java.nio.file.Path; |
| import java.nio.file.Paths; |
| import java.nio.file.StandardOpenOption; |
| import java.util.Collection; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Set; |
| import java.util.UUID; |
| |
| import javax.imageio.ImageIO; |
| import javax.imageio.ImageReader; |
| import javax.imageio.stream.ImageInputStream; |
| |
| import org.apache.batik.dom.svg.SAXSVGDocumentFactory; |
| import org.apache.batik.util.XMLResourceDescriptor; |
| import org.apache.commons.imaging.ImageFormat; |
| import org.apache.commons.imaging.ImageFormats; |
| import org.apache.commons.imaging.ImageInfo; |
| import org.apache.commons.imaging.ImageParser; |
| import org.apache.commons.imaging.ImageReadException; |
| import org.apache.commons.imaging.ImageWriteException; |
| import org.apache.commons.imaging.Imaging; |
| import org.apache.commons.imaging.formats.gif.GifImageParser; |
| import org.apache.commons.imaging.formats.jpeg.JpegImageParser; |
| import org.apache.commons.imaging.formats.png.PngImageParser; |
| import org.apache.commons.imaging.formats.tiff.TiffImageParser; |
| import org.apache.commons.io.FileUtils; |
| import org.apache.ofbiz.base.util.Debug; |
| import org.apache.ofbiz.base.util.FileUtil; |
| import org.apache.ofbiz.entity.Delegator; |
| import org.apache.ofbiz.entity.util.EntityUtilProperties; |
| import org.apache.pdfbox.pdmodel.PDDocument; |
| import org.apache.pdfbox.pdmodel.PDDocumentNameDictionary; |
| import org.apache.pdfbox.pdmodel.PDEmbeddedFilesNameTreeNode; |
| import org.apache.tika.Tika; |
| import org.apache.tika.exception.TikaException; |
| import org.apache.tika.metadata.Metadata; |
| import org.apache.tika.metadata.TikaCoreProperties; |
| import org.apache.tika.parser.AutoDetectParser; |
| import org.apache.tika.parser.ParseContext; |
| import org.apache.tika.parser.Parser; |
| import org.apache.tika.parser.RecursiveParserWrapper; |
| import org.apache.tika.sax.BasicContentHandlerFactory; |
| import org.apache.tika.sax.ContentHandlerFactory; |
| import org.apache.tika.sax.RecursiveParserWrapperHandler; |
| import org.xml.sax.SAXException; |
| |
| import com.lowagie.text.pdf.PdfReader; |
| |
| public class SecuredUpload { |
| |
| // This can be helpful: |
| // https://en.wikipedia.org/wiki/File_format |
| // https://en.wikipedia.org/wiki/List_of_file_signatures |
| // See also information in security.properties: |
| // Line #-- UPLOAD: supported file formats are *safe* PNG, GIF, TIFF, JPEG, PDF, Audio and Video and ZIP |
| |
| private static final String MODULE = SecuredUpload.class.getName(); |
| |
| /** |
| * @param fileToCheck |
| * @param fileType |
| * @return true if the file is valid |
| * @throws IOException |
| * @throws ImageReadException |
| */ |
| public static boolean isValidFile(String fileToCheck, String fileType, Delegator delegator) throws IOException, ImageReadException { |
| |
| if (("true".equalsIgnoreCase(EntityUtilProperties.getPropertyValue("security", "allowAllUploads", delegator)))) { |
| return true; |
| } |
| |
| Path p = Paths.get(fileToCheck); |
| String file = p.getFileName().toString(); |
| if (org.apache.commons.lang3.SystemUtils.IS_OS_WINDOWS) { |
| if (fileToCheck.length() > 259) { |
| Debug.logError("Uploaded file name too long", MODULE); |
| return false; |
| } else if (!file.matches("[a-zA-Z0-9]{1,249}.[a-zA-Z0-9]{1,10}")) { |
| Debug.logError("Uploaded file " |
| + " should contain only Alpha-Numeric characters, only 1 dot as an input for the file name and the extension." |
| + "The file name and the extension should not be empty at all", |
| MODULE); |
| return false; |
| } |
| } else { |
| if (fileToCheck.length() > 4096) { |
| Debug.logError("Uploaded file name too long", MODULE); |
| return false; |
| } else if (!file.matches("[a-zA-Z0-9]{1,4086}.[a-zA-Z0-9]{1,10}")) { |
| Debug.logError("Uploaded file " |
| + " should contain only Alpha-Numeric characters, only 1 dot as an input for the file name and the extension." |
| + "Tthe file name and the extension should not be empty at all", |
| MODULE); |
| return false; |
| } |
| } |
| |
| if (isExecutable(fileToCheck)) { |
| return false; |
| } |
| |
| switch (fileType) { |
| case "Image": |
| if (isValidImageFile(fileToCheck)) { |
| return true; |
| } |
| break; |
| |
| case "ImageAndSvg": |
| if (isValidImageIncludingSvgFile(fileToCheck)) { |
| return true; |
| } |
| break; |
| |
| case "PDF": |
| if (isValidPdfFile(fileToCheck)) { |
| return true; |
| } |
| break; |
| |
| case "Compressed": |
| if (isValidCompressedFile(fileToCheck, delegator)) { |
| return true; |
| } |
| break; |
| |
| case "AllButCompressed": |
| if (isValidTextFile(fileToCheck) |
| || isValidImageIncludingSvgFile(fileToCheck) |
| || isValidPdfFile(fileToCheck)) { |
| return true; |
| } |
| break; |
| |
| case "Text": |
| // The philosophy for isValidTextFile() is that |
| // we can't presume of all possible text contents used for attacks with payloads |
| // At least there is an easy way to prevent them in isValidTextFile |
| if (isValidTextFile(fileToCheck)) { |
| return true; |
| } |
| break; |
| |
| case "Audio": |
| if (isValidAudioFile(fileToCheck)) { |
| return true; |
| } |
| break; |
| case "Video": |
| if (isValidVideoFile(fileToCheck)) { |
| return true; |
| } |
| break; |
| |
| default: // All |
| if (isValidTextFile(fileToCheck) |
| || isValidImageIncludingSvgFile(fileToCheck) |
| || isValidCompressedFile(fileToCheck, delegator) |
| || isValidAudioFile(fileToCheck) |
| || isValidVideoFile(fileToCheck) |
| || isValidPdfFile(fileToCheck)) { |
| return true; |
| } |
| break; |
| } |
| Debug.logError("File :" + fileToCheck + ", can't be uploaded for security reason", MODULE); |
| File badFile = new File(fileToCheck); |
| if (!badFile.delete()) { |
| Debug.logError("File :" + fileToCheck + ", couldn't be deleted", MODULE); |
| } |
| return false; |
| } |
| |
| /** |
| * Is it a supported image format? |
| * @param fileName |
| * @return true if it's a valid image file |
| * @throws IOException ImageReadException |
| */ |
| private static boolean isValidImageFile(String fileName) throws ImageReadException, IOException { |
| Path filePath = Paths.get(fileName); |
| byte[] bytesFromFile = Files.readAllBytes(filePath); |
| ImageFormat imageFormat = Imaging.guessFormat(bytesFromFile); |
| return imageFormat.equals(ImageFormats.PNG) |
| || imageFormat.equals(ImageFormats.GIF) |
| || imageFormat.equals(ImageFormats.TIFF) |
| || imageFormat.equals(ImageFormats.JPEG) |
| && imageMadeSafe(fileName); |
| } |
| |
| /** |
| * Implementation based on https://github.com/righettod/document-upload-protection sanitizer for Image file. See |
| * https://github.com/righettod/document-upload-protection/blob/master/src/main/java/eu/righettod/poc/sanitizer/ImageDocumentSanitizerImpl.java |
| * Uses Java built-in API in complement of Apache Commons Imaging for format not supported by the built-in API. See |
| * http://commons.apache.org/proper/commons-imaging/ and http://commons.apache.org/proper/commons-imaging/formatsupport.html |
| */ |
| private static boolean imageMadeSafe(String fileName) { |
| File file = new File(fileName); |
| boolean safeState = false; |
| boolean fallbackOnApacheCommonsImaging; |
| try { |
| if ((file != null) && file.exists() && file.canRead() && file.canWrite()) { |
| // Get the image format |
| String formatName; |
| ImageInputStream iis = ImageIO.createImageInputStream(file); |
| Iterator<ImageReader> imageReaderIterator = ImageIO.getImageReaders(iis); |
| // If there not ImageReader instance found so it's means that the current format is not supported by the Java built-in API |
| if (!imageReaderIterator.hasNext()) { |
| ImageInfo imageInfo = Imaging.getImageInfo(file); |
| if (imageInfo != null && imageInfo.getFormat() != null && imageInfo.getFormat().getName() != null) { |
| formatName = imageInfo.getFormat().getName(); |
| fallbackOnApacheCommonsImaging = true; |
| } else { |
| throw new IOException("Format of the original image " + fileName + " is not supported for read operation !"); |
| } |
| } else { |
| ImageReader reader = imageReaderIterator.next(); |
| formatName = reader.getFormatName(); |
| fallbackOnApacheCommonsImaging = false; |
| iis.close(); // This was not correctly handled in the document-upload-protection example, and I did not spot it :/ |
| } |
| |
| // Load the image |
| BufferedImage originalImage; |
| if (!fallbackOnApacheCommonsImaging) { |
| originalImage = ImageIO.read(file); |
| } else { |
| originalImage = Imaging.getBufferedImage(file); |
| } |
| |
| // Check that image has been successfully loaded |
| if (originalImage == null) { |
| throw new IOException("Cannot load the original image " + fileName + "!"); |
| } |
| |
| // Get current Width and Height of the image |
| int originalWidth = originalImage.getWidth(null); |
| int originalHeight = originalImage.getHeight(null); |
| |
| // Resize the image by removing 1px on Width and Height |
| Image resizedImage = originalImage.getScaledInstance(originalWidth - 1, originalHeight - 1, Image.SCALE_SMOOTH); |
| |
| // Resize the resized image by adding 1px on Width and Height - In fact set image to is initial size |
| Image initialSizedImage = resizedImage.getScaledInstance(originalWidth, originalHeight, Image.SCALE_SMOOTH); |
| |
| // Save image by overwriting the provided source file content |
| BufferedImage sanitizedImage = new BufferedImage(initialSizedImage.getWidth(null), initialSizedImage.getHeight(null), |
| BufferedImage.TYPE_INT_RGB); |
| Graphics bg = sanitizedImage.getGraphics(); |
| bg.drawImage(initialSizedImage, 0, 0, null); |
| bg.dispose(); |
| OutputStream fos = Files.newOutputStream(file.toPath(), StandardOpenOption.WRITE); |
| if (!fallbackOnApacheCommonsImaging) { |
| ImageIO.write(sanitizedImage, formatName, fos); |
| } else { |
| ImageParser imageParser; |
| // Handle only formats for which Apache Commons Imaging can successfully write (YES in Write column of the reference link) |
| // the image format. See reference link in the class header |
| switch (formatName) { |
| case "TIFF": { |
| imageParser = new TiffImageParser(); |
| break; |
| } |
| case "GIF": { |
| imageParser = new GifImageParser(); |
| break; |
| } |
| case "PNG": { |
| imageParser = new PngImageParser(); |
| break; |
| } |
| case "JPEG": { |
| imageParser = new JpegImageParser(); |
| break; |
| } |
| default: { |
| throw new IOException("Format of the original image " + fileName + " is not supported for write operation !"); |
| } |
| } |
| imageParser.writeImage(sanitizedImage, fos, new HashMap<>()); |
| } |
| // Set state flag |
| fos.close(); // This was not correctly handled in the document-upload-protection example, and I did not spot it :/ |
| safeState = true; |
| } |
| } catch (IOException | ImageReadException | ImageWriteException e) { |
| safeState = false; |
| Debug.logWarning(e, "Error during Image file " + fileName + " processing !", MODULE); |
| } |
| return safeState; |
| } |
| |
| /** |
| * Is it a supported image format, including SVG? |
| * @param fileName |
| * @return true if it's a valid image file |
| * @throws IOException ImageReadException |
| */ |
| private static boolean isValidImageIncludingSvgFile(String fileName) throws ImageReadException, IOException { |
| return isValidImageFile(fileName) || isValidSvgFile(fileName); |
| } |
| |
| /** |
| * Is it an SVG file? |
| * @param fileName |
| * @return true if it's a valid SVG file |
| * @throws IOException |
| */ |
| private static boolean isValidSvgFile(String fileName) throws IOException { |
| String mimeType = getMimeTypeFromFileName(fileName); |
| if ("image/svg+xml".equals(mimeType)) { |
| Path filePath = Paths.get(fileName); |
| String parser = XMLResourceDescriptor.getXMLParserClassName(); |
| SAXSVGDocumentFactory f = new SAXSVGDocumentFactory(parser); |
| try { |
| f.createDocument(filePath.toUri().toString()); |
| } catch (IOException e) { |
| return false; |
| } |
| return isValidTextFile(fileName); // Validate content to prevent webshell |
| } |
| return false; |
| } |
| |
| /** |
| * @param fileName |
| * @return true if it's a safe PDF file: is PDF and does not contains embedded files |
| * @throws IOException If there is an error parsing the document |
| */ |
| private static boolean isValidPdfFile(String fileName) throws IOException { |
| File file = new File(fileName); |
| boolean safeState = false; |
| try { |
| if ((file != null) && file.exists()) { |
| // Load stream in PDF parser |
| // If the stream is not a PDF then exception will be thrown and safe state will be set to FALSE |
| PdfReader reader = new PdfReader(file.getAbsolutePath()); |
| // Check 1: detect if the document contains any JavaScript code |
| String jsCode = reader.getJavaScript(); |
| if (jsCode == null) { |
| // OK no JS code, pass to check 2: detect if the document has any embedded files |
| PDEmbeddedFilesNameTreeNode efTree = null; |
| try (PDDocument pdDocument = PDDocument.load(file)) { |
| PDDocumentNameDictionary names = new PDDocumentNameDictionary(pdDocument.getDocumentCatalog()); |
| efTree = names.getEmbeddedFiles(); |
| } |
| safeState = efTree == null; |
| } |
| } |
| } catch (Exception e) { |
| safeState = false; |
| Debug.logError(e, "for security reason the PDF file " + file.getAbsolutePath() + "can't be uploaded !", MODULE); |
| } |
| return safeState; |
| } |
| |
| private static boolean isExecutable(String fileName) throws IOException { |
| String mimeType = getMimeTypeFromFileName(fileName); |
| // Check for Windows executable. Neglect .bat and .ps1: https://s.apache.org/c8sim |
| if ("application/x-msdownload".equals(mimeType) || "application/x-ms-installer".equals(mimeType)) { |
| Debug.logError("The file" + fileName + " is a Windows executable, for security reason it's not accepted :", MODULE); |
| return true; |
| } |
| // Check for ELF (Linux) and scripts |
| if ("application/x-elf".equals(mimeType) |
| || "application/x-sh".equals(mimeType) |
| || "application/text/x-perl".equals(mimeType) |
| || "application/text/x-python".equals(mimeType)) { |
| Debug.logError("The file" + fileName + " is a Linux executable, for security reason it's not accepted :", MODULE); |
| return true; |
| } |
| return false; |
| } |
| |
| /** |
| * Check if the compressed file is valid Does not handle compressed files in sub folders of compressed files. Handles only ZIP files, if you need |
| * bzip, rar, tar or/and 7z file formats they can be handled by Apache commons-compress: Types based on |
| * https://developer.mozilla.org/fr/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types For code explanations see |
| * http://commons.apache.org/proper/commons-compress/examples.html |
| * @param fileName |
| * @return true if it's a valid compressed file |
| * @throws IOException ImageReadException |
| */ |
| private static boolean isValidCompressedFile(String fileName, Delegator delegator) throws IOException, ImageReadException { |
| String mimeType = getMimeTypeFromFileName(fileName); |
| // I planned to handle more formats but did only ZIP |
| // The code can be extended based on that |
| // if ("application/octet-stream".equals(mimeType) |
| // || "application/x-bzip".equals(mimeType) |
| // || "application/x-bzip2".equals(mimeType) |
| // || "application/java-archive".equals(mimeType) |
| // || "application/x-rar-compressed".equals(mimeType) |
| // || "application/x-tar".equals(mimeType) |
| // || "application/zip".equals(mimeType) |
| // || "application/x-zip-compressed".equals(mimeType) |
| // || "multipart/x-zip".equals(mimeType) |
| // || "application/x-7z-compressed".equals(mimeType)) { |
| |
| // Handles only Zip format OOTB |
| File fileToCheck = new File(fileName); |
| String folderName = fileToCheck.getParentFile().toString() + File.separator + UUID.randomUUID(); |
| if ("application/octet-stream".equals(mimeType) |
| || "application/java-archive".equals(mimeType) |
| || "application/zip".equals(mimeType) |
| || "application/x-zip-compressed".equals(mimeType) |
| || "multipart/x-zip".equals(mimeType)) { |
| if (!FileUtil.unZip(fileName, folderName, "")) { |
| return false; |
| } else { |
| // Keep it like that to allow to spot other file types which could be included... |
| // try { |
| // recursiveParserWrapper(fileName); |
| // } catch (SAXException | TikaException e) { |
| // // TODO Auto-generated catch block |
| // e.printStackTrace(); |
| // } |
| // Recursive method to check inside directories |
| return isValidDirectoryInCompressedFile(folderName, delegator); |
| } |
| } |
| return false; |
| } |
| |
| /* |
| * According to http://tika.apache.org/1.24.1/detection.html#The_default_Tika_Detector The simplest way to detect is through the Tika Facade |
| * class, which provides methods to detect based on File, InputStream, InputStream and Filename, Filename or a few others. It works best with a |
| * File or TikaInputStream. |
| * @param fileName |
| * @return true if the file is valid |
| */ |
| private static String getMimeTypeFromFileName(String fileName) throws IOException { |
| File file = new File(fileName); |
| Tika tika = new Tika(); |
| return tika.detect(file); |
| } |
| |
| private static boolean isValidDirectoryInCompressedFile(String folderName, Delegator delegator) throws IOException, ImageReadException { |
| File folder = new File(folderName); |
| Collection<File> files = FileUtils.listFiles(folder, null, true); |
| for (File f : files) { |
| if (f.isDirectory()) { |
| Collection<File> dirInside = FileUtils.listFiles(f, null, true); |
| for (File insideFile : dirInside) { |
| if (!isValidDirectoryInCompressedFile(insideFile.getAbsolutePath(), delegator)) { |
| FileUtils.deleteDirectory(folder); |
| return false; |
| } |
| } |
| } else if (!isValidFile(f.getAbsolutePath(), "AllButCompressed", delegator)) { |
| FileUtils.deleteDirectory(folder); |
| return false; |
| } |
| } |
| FileUtils.deleteDirectory(folder); |
| return true; |
| } |
| |
| /** |
| * For documents that may contain embedded documents, it might be helpful to create list of metadata objects, one for the container document and |
| * one for each embedded document. This allows easy access to both the extracted content and the metadata of each embedded document. Note that |
| * many document formats can contain embedded documents, including traditional container formats -- zip, tar and others -- but also common office |
| * document formats including: MSWord, MSExcel, MSPowerPoint, RTF, PDF, MSG and several others. |
| * <p> |
| * The "content" format is determined by the ContentHandlerFactory, and the content is stored in |
| * {@link org.apache.tika.parser.RecursiveParserWrapper#TIKA_CONTENT} |
| * <p> |
| * The drawback to the RecursiveParserWrapper is that it caches metadata and contents in memory. This should not be used on files whose contents |
| * are too big to be handled in memory. |
| * @return a list of metadata object, one each for the container file and each embedded file |
| * @throws IOException |
| * @throws SAXException |
| * @throws TikaException |
| */ |
| // This can turn to be useful, so I let it there... |
| // Inspired by https://cwiki.apache.org/confluence/display/tika/RecursiveMetadata |
| // And https://stackoverflow.com/questions/62132310/apache-tika-exctract-file-names-and-mime-types-from-archive |
| @SuppressWarnings("unused") |
| private static Set<String> recursiveParserWrapper(String fileName) throws IOException, SAXException, TikaException { |
| File file = new File(fileName); |
| Parser p = new AutoDetectParser(); |
| ContentHandlerFactory factory = new BasicContentHandlerFactory(BasicContentHandlerFactory.HANDLER_TYPE.IGNORE, -1); |
| RecursiveParserWrapper wrapper = new RecursiveParserWrapper(p); |
| Metadata metadata = new Metadata(); |
| metadata.set(TikaCoreProperties.ORIGINAL_RESOURCE_NAME, file.getName()); |
| ParseContext context = new ParseContext(); |
| RecursiveParserWrapperHandler handler = new RecursiveParserWrapperHandler(factory, -1); |
| try (InputStream stream = new FileInputStream(file)) { |
| wrapper.parse(stream, handler, metadata, context); |
| } |
| List<Metadata> metadatas = handler.getMetadataList(); |
| Set<String> mimeTypes = new HashSet<>(); |
| for (Metadata metadata1 : metadatas) { |
| mimeTypes.add(metadata1.get(Metadata.CONTENT_TYPE)); |
| } |
| return mimeTypes; |
| } |
| |
| /** |
| * Is this a valid Audio file? |
| * @param fileName must be an UTF-8 encoded text file |
| * @return true if it's a valid Audio file? |
| * @throws IOException |
| */ |
| private static boolean isValidAudioFile(String fileName) throws IOException { |
| String mimeType = getMimeTypeFromFileName(fileName); |
| if ("audio/basic".equals(mimeType) |
| || "audio/wav".equals(mimeType) |
| || "audio/x-ms-wax".equals(mimeType) |
| || "audio/mpeg".equals(mimeType) |
| || "audio/mp4".equals(mimeType) |
| || "audio/ogg".equals(mimeType) |
| || "audio/vorbis".equals(mimeType) |
| || "audio/x-ogg".equals(mimeType) |
| || "audio/flac".equals(mimeType) |
| || "audio/x-flac".equals(mimeType)) { |
| return true; |
| } |
| Debug.logError("The file" + fileName + " is not a valid audio file, for security reason it's not accepted :", MODULE); |
| return false; |
| } |
| |
| /** |
| * Is this a valid Audio file? |
| * @param fileName must be an UTF-8 encoded text file |
| * @return true if it's a valid Audio file? |
| * @throws IOException |
| */ |
| private static boolean isValidVideoFile(String fileName) throws IOException { |
| String mimeType = getMimeTypeFromFileName(fileName); |
| if ("video/avi".equals(mimeType) |
| || "video/mpeg".equals(mimeType) |
| || "video/mp4".equals(mimeType) |
| || "video/quicktime".equals(mimeType) |
| || "video/3gpp".equals(mimeType) |
| || "video/x-ms-asf".equals(mimeType) |
| || "video/x-flv".equals(mimeType) |
| || "video/x-ms-wvx".equals(mimeType) |
| || "video/x-ms-wm".equals(mimeType) |
| || "video/x-ms-wmv".equals(mimeType) |
| || "video/x-ms-wmx".equals(mimeType)) { |
| return true; |
| } |
| Debug.logError("The file" + fileName + " is not a valid video file, for security reason it's not accepted :", MODULE); |
| return false; |
| } |
| |
| /** |
| * Does this text file contains a Freemarker Server-Side Template Injection (SSTI) using freemarker.template.utility.Execute? Etc. |
| * @param fileName must be an UTF-8 encoded text file |
| * @return true if the text file does not contains a Freemarker SSTI |
| * @throws IOException |
| */ |
| private static boolean isValidTextFile(String fileName) throws IOException { |
| Path filePath = Paths.get(fileName); |
| byte[] bytesFromFile = Files.readAllBytes(filePath); |
| try { |
| Charset.availableCharsets().get("UTF-8").newDecoder().decode(ByteBuffer.wrap(bytesFromFile)); |
| } catch (CharacterCodingException e) { |
| return false; |
| } |
| String content = new String(bytesFromFile); |
| return !(content.toLowerCase().contains("freemarker") // Should be OK, should not be used in Freemarker templates, not part of the syntax. |
| // Else "template.utility.Execute" is a good replacement but not as much catching, who |
| // knows... |
| || content.toLowerCase().contains("import=\"java") |
| || content.toLowerCase().contains("Runtime.getRuntime().exec(") |
| || content.toLowerCase().contains("<%@ page") |
| || content.toLowerCase().contains("<script") |
| || content.toLowerCase().contains("<body>") |
| || content.toLowerCase().contains("<form") |
| || content.toLowerCase().contains("php") |
| || content.toLowerCase().contains("javascript") |
| || content.toLowerCase().contains("%eval") |
| || content.toLowerCase().contains("@eval") |
| || content.toLowerCase().contains("import os") // Python |
| || content.toLowerCase().contains("passthru") |
| || content.toLowerCase().contains("exec") |
| || content.toLowerCase().contains("shell_exec") |
| || content.toLowerCase().contains("assert") |
| || content.toLowerCase().contains("str_rot13") |
| || content.toLowerCase().contains("system") |
| || content.toLowerCase().contains("phpinfo") |
| || content.toLowerCase().contains("base64_decode") |
| || content.toLowerCase().contains("chmod") |
| || content.toLowerCase().contains("mkdir") |
| || content.toLowerCase().contains("fopen") |
| || content.toLowerCase().contains("fclose") |
| || content.toLowerCase().contains("new file") |
| || content.toLowerCase().contains("import") |
| || content.toLowerCase().contains("upload") |
| || content.toLowerCase().contains("getFileName") |
| || content.toLowerCase().contains("Download") |
| || content.toLowerCase().contains("getOutputString") |
| || content.toLowerCase().contains("readfile")); |
| // TODO.... to be continued with known webshell contents... a complete allow list is impossible anyway... |
| // eg: https://www.acunetix.com/blog/articles/detection-prevention-introduction-web-shells-part-5/ |
| } |
| } |