001package eu.righettod; 002 003 004import com.auth0.jwt.interfaces.DecodedJWT; 005import org.apache.commons.csv.CSVFormat; 006import org.apache.commons.csv.CSVRecord; 007import org.apache.commons.imaging.ImageInfo; 008import org.apache.commons.imaging.Imaging; 009import org.apache.commons.imaging.common.ImageMetadata; 010import org.apache.commons.validator.routines.EmailValidator; 011import org.apache.commons.validator.routines.InetAddressValidator; 012import org.apache.pdfbox.Loader; 013import org.apache.pdfbox.pdmodel.PDDocument; 014import org.apache.pdfbox.pdmodel.PDDocumentCatalog; 015import org.apache.pdfbox.pdmodel.PDDocumentInformation; 016import org.apache.pdfbox.pdmodel.PDDocumentNameDictionary; 017import org.apache.pdfbox.pdmodel.common.PDMetadata; 018import org.apache.pdfbox.pdmodel.interactive.action.*; 019import org.apache.pdfbox.pdmodel.interactive.annotation.AnnotationFilter; 020import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation; 021import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationLink; 022import org.apache.poi.poifs.filesystem.DirectoryEntry; 023import org.apache.poi.poifs.filesystem.POIFSFileSystem; 024import org.apache.poi.poifs.macros.VBAMacroReader; 025import org.apache.tika.detect.DefaultDetector; 026import org.apache.tika.detect.Detector; 027import org.apache.tika.io.TemporaryResources; 028import org.apache.tika.io.TikaInputStream; 029import org.apache.tika.metadata.Metadata; 030import org.apache.tika.mime.MediaType; 031import org.apache.tika.mime.MimeTypes; 032import org.w3c.dom.Document; 033import org.xml.sax.EntityResolver; 034import org.xml.sax.InputSource; 035import org.xml.sax.SAXException; 036 037import javax.crypto.Mac; 038import javax.crypto.spec.SecretKeySpec; 039import javax.imageio.ImageIO; 040import javax.json.Json; 041import javax.json.JsonReader; 042import javax.xml.XMLConstants; 043import javax.xml.parsers.DocumentBuilder; 044import javax.xml.parsers.DocumentBuilderFactory; 045import javax.xml.parsers.ParserConfigurationException; 046import javax.xml.stream.XMLInputFactory; 047import javax.xml.stream.XMLStreamReader; 048import javax.xml.stream.events.XMLEvent; 049import javax.xml.validation.Schema; 050import javax.xml.validation.SchemaFactory; 051import java.awt.*; 052import java.awt.image.BufferedImage; 053import java.io.*; 054import java.net.*; 055import java.net.http.HttpClient; 056import java.net.http.HttpRequest; 057import java.net.http.HttpResponse; 058import java.nio.ByteBuffer; 059import java.nio.charset.Charset; 060import java.nio.charset.StandardCharsets; 061import java.nio.file.Files; 062import java.security.MessageDigest; 063import java.security.SecureRandom; 064import java.time.Duration; 065import java.util.*; 066import java.util.List; 067import java.util.concurrent.*; 068import java.util.concurrent.atomic.AtomicInteger; 069import java.util.regex.Pattern; 070import java.util.zip.ZipEntry; 071import java.util.zip.ZipFile; 072 073/** 074 * Provides different utilities methods to apply processing from a security perspective.<br> 075 * These code snippet: 076 * <ul> 077 * <li>Can be used, as "foundation", to customize the validation to the app context.</li> 078 * <li>Were implemented in a way to facilitate adding or removal of validations depending on usage context.</li> 079 * <li>Were centralized on one class to be able to enhance them across time as well as <a href="https://github.com/righettod/code-snippets-security-utils/issues">missing case/bug identification</a>.</li> 080 * </ul> 081 */ 082public class SecurityUtils { 083 /** 084 * Default constructor: Not needed as the class only provides static methods. 085 */ 086 private SecurityUtils() { 087 } 088 089 /** 090 * Apply a collection of validation to verify if a provided PIN code is considered weak (easy to guess) or none.<br> 091 * This method consider that format of the PIN code is [0-9]{6,}<br> 092 * Rule to consider a PIN code as weak: 093 * <ul> 094 * <li>Length is inferior to 6 positions.</li> 095 * <li>Contain only the same number or only a sequence of zero.</li> 096 * <li>Contain sequence of following incremental or decremental numbers.</li> 097 * </ul> 098 * 099 * @param pinCode PIN code to verify. 100 * @return True only if the PIN is considered as weak. 101 */ 102 public static boolean isWeakPINCode(String pinCode) { 103 boolean isWeak = true; 104 //Length is inferior to 6 positions 105 //Use "Long.parseLong(pinCode)" to cause a NumberFormatException if the PIN is not a numeric one 106 //and to ensure that the PIN is not only a sequence of zero 107 if (pinCode != null && Long.parseLong(pinCode) > 0 && pinCode.trim().length() > 5) { 108 //Contain only the same number 109 String regex = String.format("^[%s]{%s}$", pinCode.charAt(0), pinCode.length()); 110 if (!Pattern.matches(regex, pinCode)) { 111 //Contain sequence of following incremental or decremental numbers 112 char previousChar = 'X'; 113 boolean containSequence = false; 114 for (char c : pinCode.toCharArray()) { 115 if (previousChar != 'X') { 116 int previousNbr = Integer.parseInt(String.valueOf(previousChar)); 117 int currentNbr = Integer.parseInt(String.valueOf(c)); 118 if (currentNbr == (previousNbr - 1) || currentNbr == (previousNbr + 1)) { 119 containSequence = true; 120 break; 121 } 122 } 123 previousChar = c; 124 } 125 if (!containSequence) { 126 isWeak = false; 127 } 128 } 129 } 130 return isWeak; 131 } 132 133 /** 134 * Apply a collection of validations on a Word 97-2003 (binary format) document file provided: 135 * <ul> 136 * <li>Real Microsoft Word 97-2003 document file.</li> 137 * <li>No VBA Macro.<br></li> 138 * <li>No embedded objects.</li> 139 * </ul> 140 * 141 * @param wordFilePath Filename of the Word document file to check. 142 * @return True only if the file pass all validations. 143 * @see "https://poi.apache.org/components/" 144 * @see "https://poi.apache.org/components/document/" 145 * @see "https://poi.apache.org/components/poifs/how-to.html" 146 * @see "https://poi.apache.org/components/poifs/embeded.html" 147 * @see "https://poi.apache.org/" 148 * @see "https://mvnrepository.com/artifact/org.apache.poi/poi" 149 */ 150 public static boolean isWord972003DocumentSafe(String wordFilePath) { 151 boolean isSafe = false; 152 try { 153 File wordFile = new File(wordFilePath); 154 if (wordFile.exists() && wordFile.canRead() && wordFile.isFile()) { 155 //Step 1: Try to load the file, if its fail then it imply that is not a valid Word 97-2003 format file 156 try (POIFSFileSystem fs = new POIFSFileSystem(wordFile)) { 157 //Step 2: Check if the document contains VBA macros, in our case is not allowed 158 VBAMacroReader macroReader = new VBAMacroReader(fs); 159 Map<String, String> macros = macroReader.readMacros(); 160 if (macros == null || macros.isEmpty()) { 161 //Step 3: Check if the document contains any embedded objects, in our case is not allowed 162 //From POI documentation: 163 //Word normally stores embedded files in subdirectories of the ObjectPool directory, itself a subdirectory of the filesystem root. 164 //Typically, these subdirectories and named starting with an underscore, followed by 10 numbers. 165 final List<String> embeddedObjectFound = new ArrayList<>(); 166 DirectoryEntry root = fs.getRoot(); 167 if (root.getEntryCount() > 0) { 168 root.iterator().forEachRemaining(entry -> { 169 if ("ObjectPool".equalsIgnoreCase(entry.getName()) && entry instanceof DirectoryEntry) { 170 DirectoryEntry objPoolDirectory = (DirectoryEntry) entry; 171 if (objPoolDirectory.getEntryCount() > 0) { 172 objPoolDirectory.iterator().forEachRemaining(objPoolDirectoryEntry -> { 173 if (objPoolDirectoryEntry instanceof DirectoryEntry) { 174 DirectoryEntry objPoolDirectoryEntrySubDirectoryEntry = (DirectoryEntry) objPoolDirectoryEntry; 175 if (objPoolDirectoryEntrySubDirectoryEntry.getEntryCount() > 0) { 176 objPoolDirectoryEntrySubDirectoryEntry.forEach(objPoolDirectoryEntrySubDirectoryEntryEntry -> { 177 if (objPoolDirectoryEntrySubDirectoryEntryEntry.isDocumentEntry()) { 178 embeddedObjectFound.add(objPoolDirectoryEntrySubDirectoryEntryEntry.getName()); 179 } 180 }); 181 } 182 } 183 }); 184 } 185 } 186 }); 187 } 188 isSafe = embeddedObjectFound.isEmpty(); 189 } 190 } 191 } 192 } catch (Exception e) { 193 isSafe = false; 194 } 195 return isSafe; 196 } 197 198 /** 199 * Ensure that an XML file does not contain any External Entity, DTD or XInclude instructions. 200 * 201 * @param xmlFilePath Filename of the XML file to check. 202 * @return True only if the file pass all validations. 203 * @see "https://portswigger.net/web-security/xxe" 204 * @see "https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html#java" 205 * @see "https://docs.oracle.com/en/java/javase/13/security/java-api-xml-processing-jaxp-security-guide.html#GUID-82F8C206-F2DF-4204-9544-F96155B1D258" 206 * @see "https://www.w3.org/TR/xinclude-11/" 207 * @see "https://en.wikipedia.org/wiki/XInclude" 208 */ 209 public static boolean isXMLSafe(String xmlFilePath) { 210 boolean isSafe = false; 211 try { 212 File xmlFile = new File(xmlFilePath); 213 if (xmlFile.exists() && xmlFile.canRead() && xmlFile.isFile()) { 214 //Step 1a: Verify that the XML file content does not contain any XInclude instructions 215 boolean containXInclude = Files.readAllLines(xmlFile.toPath()).stream().anyMatch(line -> line.toLowerCase(Locale.ROOT).contains(":include ")); 216 if (!containXInclude) { 217 //Step 1b: Parse the XML file, if an exception occur than it's imply that the XML specified is not a valid ones 218 //Create an XML document builder throwing Exception if a DOCTYPE instruction is present 219 DocumentBuilderFactory dbfInstance = DocumentBuilderFactory.newInstance(); 220 dbfInstance.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); 221 //Xerces 2 only 222 //dbfInstance.setFeature("http://xerces.apache.org/xerces2-j/features.html#disallow-doctype-decl",true); 223 dbfInstance.setXIncludeAware(false); 224 DocumentBuilder builder = dbfInstance.newDocumentBuilder(); 225 //Parse the document 226 Document doc = builder.parse(xmlFile); 227 isSafe = (doc != null && doc.getDocumentElement() != null); 228 } 229 } 230 } catch (Exception e) { 231 isSafe = false; 232 } 233 return isSafe; 234 } 235 236 237 /** 238 * Extract all URL links from a PDF file provided.<br> 239 * This can be used to apply validation on a PDF against contained links. 240 * 241 * @param pdfFilePath pdfFilePath Filename of the PDF file to process. 242 * @return A List of URL objects that is empty if no links is found. 243 * @throws Exception If any error occurs during the processing of the PDF file. 244 * @see "https://www.gushiciku.cn/pl/21KQ" 245 * @see "https://pdfbox.apache.org/" 246 * @see "https://mvnrepository.com/artifact/org.apache.pdfbox/pdfbox" 247 */ 248 public static List<URL> extractAllPDFLinks(String pdfFilePath) throws Exception { 249 final List<URL> links = new ArrayList<>(); 250 File pdfFile = new File(pdfFilePath); 251 try (PDDocument document = Loader.loadPDF(pdfFile)) { 252 PDDocumentCatalog documentCatalog = document.getDocumentCatalog(); 253 AnnotationFilter actionURIAnnotationFilter = new AnnotationFilter() { 254 @Override 255 public boolean accept(PDAnnotation annotation) { 256 boolean keep = false; 257 if (annotation instanceof PDAnnotationLink) { 258 keep = (((PDAnnotationLink) annotation).getAction() instanceof PDActionURI); 259 } 260 return keep; 261 } 262 }; 263 documentCatalog.getPages().forEach(page -> { 264 try { 265 page.getAnnotations(actionURIAnnotationFilter).forEach(annotation -> { 266 PDActionURI linkAnnotation = (PDActionURI) ((PDAnnotationLink) annotation).getAction(); 267 try { 268 URL urlObj = new URL(linkAnnotation.getURI()); 269 if (!links.contains(urlObj)) { 270 links.add(urlObj); 271 } 272 } catch (MalformedURLException e) { 273 throw new RuntimeException(e); 274 } 275 }); 276 } catch (Exception e) { 277 throw new RuntimeException(e); 278 } 279 }); 280 } 281 return links; 282 } 283 284 /** 285 * Apply a collection of validations on a PDF file provided: 286 * <ul> 287 * <li>Real PDF file.</li> 288 * <li>No attachments.</li> 289 * <li>No Javascript code.</li> 290 * <li>No links using action of type URI/Launch/RemoteGoTo/ImportData.</li> 291 * </ul> 292 * 293 * @param pdfFilePath Filename of the PDF file to check. 294 * @return True only if the file pass all validations. 295 * @see "https://stackoverflow.com/a/36161267" 296 * @see "https://www.gushiciku.cn/pl/21KQ" 297 * @see "https://github.com/jonaslejon/malicious-pdf" 298 * @see "https://pdfbox.apache.org/" 299 * @see "https://mvnrepository.com/artifact/org.apache.pdfbox/pdfbox" 300 */ 301 public static boolean isPDFSafe(String pdfFilePath) { 302 boolean isSafe = false; 303 try { 304 File pdfFile = new File(pdfFilePath); 305 if (pdfFile.exists() && pdfFile.canRead() && pdfFile.isFile()) { 306 //Step 1: Try to load the file, if its fail then it imply that is not a valid PDF file 307 try (PDDocument document = Loader.loadPDF(pdfFile)) { 308 //Step 2: Check if the file contains attached files, in our case is not allowed 309 PDDocumentCatalog documentCatalog = document.getDocumentCatalog(); 310 PDDocumentNameDictionary namesDictionary = new PDDocumentNameDictionary(documentCatalog); 311 if (namesDictionary.getEmbeddedFiles() == null) { 312 //Step 3: Check if the file contains Javascript code, in our case is not allowed 313 if (namesDictionary.getJavaScript() == null) { 314 //Step 4: Check if the file contains links using action of type URI/Launch/RemoteGoTo/ImportData, in our case is not allowed 315 final List<Integer> notAllowedAnnotationCounterList = new ArrayList<>(); 316 AnnotationFilter notAllowedAnnotationFilter = new AnnotationFilter() { 317 @Override 318 public boolean accept(PDAnnotation annotation) { 319 boolean keep = false; 320 if (annotation instanceof PDAnnotationLink) { 321 PDAnnotationLink link = (PDAnnotationLink) annotation; 322 PDAction action = link.getAction(); 323 if ((action instanceof PDActionURI) || (action instanceof PDActionLaunch) || (action instanceof PDActionRemoteGoTo) || (action instanceof PDActionImportData)) { 324 keep = true; 325 } 326 } 327 return keep; 328 } 329 }; 330 documentCatalog.getPages().forEach(page -> { 331 try { 332 notAllowedAnnotationCounterList.add(page.getAnnotations(notAllowedAnnotationFilter).size()); 333 } catch (IOException e) { 334 throw new RuntimeException(e); 335 } 336 }); 337 if (notAllowedAnnotationCounterList.stream().reduce(0, Integer::sum) == 0) { 338 isSafe = true; 339 } 340 } 341 } 342 } 343 } 344 } catch (Exception e) { 345 isSafe = false; 346 } 347 return isSafe; 348 } 349 350 /** 351 * Remove as much as possible metadata from the provided PDF document object. 352 * 353 * @param document PDFBox PDF document object on which metadata must be removed. 354 * @see "https://gist.github.com/righettod/d7e07443c43d393a39de741a0d920069" 355 * @see "https://pdfbox.apache.org/" 356 * @see "https://mvnrepository.com/artifact/org.apache.pdfbox/pdfbox" 357 */ 358 public static void clearPDFMetadata(PDDocument document) { 359 if (document != null) { 360 PDDocumentInformation infoEmpty = new PDDocumentInformation(); 361 document.setDocumentInformation(infoEmpty); 362 PDMetadata newMetadataEmpty = new PDMetadata(document); 363 document.getDocumentCatalog().setMetadata(newMetadataEmpty); 364 } 365 } 366 367 368 /** 369 * Validate that the URL provided is really a relative URL. 370 * 371 * @param targetUrl URL to validate. 372 * @return True only if the file pass all validations. 373 * @see "https://portswigger.net/web-security/ssrf" 374 * @see "https://stackoverflow.com/q/6785442" 375 */ 376 public static boolean isRelativeURL(String targetUrl) { 377 boolean isValid = false; 378 //Reject any URL encoded content and URL starting with a double slash 379 //Reject any URL contains credentials or fragment to prevent potential bypasses 380 String work = targetUrl; 381 if (!work.contains("%") && !work.contains("@") && !work.contains("#") && !work.startsWith("//")) { 382 //Creation of a URL object must fail 383 try { 384 new URL(work); 385 isValid = false; 386 } catch (MalformedURLException mf) { 387 //Last check to be sure (for prod usage compile the pattern one time) 388 isValid = Pattern.compile("^/[a-z0-9]+", Pattern.CASE_INSENSITIVE).matcher(work).find(); 389 } 390 } 391 return isValid; 392 } 393 394 /** 395 * Apply a collection of validations on a ZIP file provided: 396 * <ul> 397 * <li>Real ZIP file.</li> 398 * <li>Contain less than a specified level of deepness.</li> 399 * <li>Do not contain Zip-Slip entry path.</li> 400 * </ul> 401 * 402 * @param zipFilePath Filename of the ZIP file to check. 403 * @param maxLevelDeepness Threshold of deepness above which a ZIP archive will be rejected. 404 * @param rejectArchiveFile Flag to specify if presence of any archive entry will cause the rejection of the ZIP file. 405 * @return True only if the file pass all validations. 406 * @see "https://rules.sonarsource.com/java/type/Security%20Hotspot/RSPEC-5042" 407 * @see "https://security.snyk.io/research/zip-slip-vulnerability" 408 * @see "https://en.wikipedia.org/wiki/Zip_bomb" 409 * @see "https://github.com/ptoomey3/evilarc" 410 * @see "https://github.com/abdulfatir/ZipBomb" 411 * @see "https://www.baeldung.com/cs/zip-bomb" 412 * @see "https://thesecurityvault.com/attacks-with-zip-files-and-mitigations/" 413 * @see "https://wiki.sei.cmu.edu/confluence/display/java/IDS04-J.+Safely+extract+files+from+ZipInputStream" 414 */ 415 public static boolean isZIPSafe(String zipFilePath, int maxLevelDeepness, boolean rejectArchiveFile) { 416 List<String> archiveExtensions = Arrays.asList("zip", "tar", "7z", "gz", "jar", "phar", "bz2", "tgz"); 417 boolean isSafe = false; 418 try { 419 File zipFile = new File(zipFilePath); 420 if (zipFile.exists() && zipFile.canRead() && zipFile.isFile() && maxLevelDeepness > 0) { 421 //Step 1: Try to load the file, if its fail then it imply that is not a valid ZIP file 422 try (ZipFile zipArch = new ZipFile(zipFile)) { 423 //Step 2: Parse entries 424 long deepness = 0; 425 ZipEntry zipEntry; 426 String entryExtension; 427 String zipEntryName; 428 boolean validationsFailed = false; 429 Enumeration<? extends ZipEntry> entries = zipArch.entries(); 430 while (entries.hasMoreElements()) { 431 zipEntry = entries.nextElement(); 432 zipEntryName = zipEntry.getName(); 433 entryExtension = zipEntryName.substring(zipEntryName.lastIndexOf(".") + 1).toLowerCase(Locale.ROOT).trim(); 434 //Step 2a: Check if the current entry is an archive file 435 if (rejectArchiveFile && archiveExtensions.contains(entryExtension)) { 436 validationsFailed = true; 437 break; 438 } 439 //Step 2b: Check that level of deepness is inferior to the threshold specified 440 if (zipEntryName.contains("/")) { 441 //Determine deepness by inspecting the entry name. 442 //Indeed, folder will be represented like this: folder/folder/folder/ 443 //So we can count the number of "/" to identify the deepness of the entry 444 deepness = zipEntryName.chars().filter(ch -> ch == '/').count(); 445 if (deepness > maxLevelDeepness) { 446 validationsFailed = true; 447 break; 448 } 449 } 450 //Step 2c: Check if any entries match pattern of zip slip payload 451 if (zipEntryName.contains("..\\") || zipEntryName.contains("../")) { 452 validationsFailed = true; 453 break; 454 } 455 } 456 if (!validationsFailed) { 457 isSafe = true; 458 } 459 } 460 } 461 } catch (Exception e) { 462 isSafe = false; 463 } 464 return isSafe; 465 } 466 467 /** 468 * Identify the mime type of the content specified (array of bytes).<br> 469 * Note that it cannot be fully trusted (see the tweet '1595824709186519041' referenced), so, additional validations are required. 470 * 471 * @param content The content as an array of bytes. 472 * @return The mime type in lower case or null if it cannot be identified. 473 * @see "https://twitter.com/righettod/status/1595824709186519041" 474 * @see "https://tika.apache.org/" 475 * @see "https://mvnrepository.com/artifact/org.apache.tika/tika-core" 476 * @see "https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types" 477 * @see "https://www.iana.org/assignments/media-types/media-types.xhtml" 478 */ 479 public static String identifyMimeType(byte[] content) { 480 String mimeType = null; 481 if (content != null && content.length > 0) { 482 Detector detector = new DefaultDetector(MimeTypes.getDefaultMimeTypes()); 483 Metadata metadata = new Metadata(); 484 try { 485 try (TemporaryResources temporaryResources = new TemporaryResources(); TikaInputStream tikaInputStream = TikaInputStream.get(new ByteArrayInputStream(content), temporaryResources, metadata)) { 486 MediaType mt = detector.detect(tikaInputStream, metadata); 487 if (mt != null) { 488 mimeType = mt.toString().toLowerCase(Locale.ROOT); 489 } 490 } 491 } catch (IOException ioe) { 492 mimeType = null; 493 } 494 } 495 return mimeType; 496 } 497 498 /** 499 * Apply a collection of validations on a string expected to be an public IP address: 500 * <ul> 501 * <li>Is a valid IP v4 or v6 address.</li> 502 * <li>Is public from an Internet perspective.</li> 503 * </ul> 504 * <br> 505 * <b>Note:</b> I often see missing such validation in the value read from HTTP request headers like "X-Forwarded-For" or "Forwarded". 506 * <br><br> 507 * <b>Note for IPv6:</b> I used documentation found so it is really experimental! 508 * 509 * @param ip String expected to be a valid IP address. 510 * @return True only if the string pass all validations. 511 * @see "https://commons.apache.org/proper/commons-validator/" 512 * @see "https://commons.apache.org/proper/commons-validator/apidocs/org/apache/commons/validator/routines/InetAddressValidator.html" 513 * @see "https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html" 514 * @see "https://cheatsheetseries.owasp.org/assets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet_Orange_Tsai_Talk.pdf" 515 * @see "https://cheatsheetseries.owasp.org/assets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet_SSRF_Bible.pdf" 516 * @see "https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For" 517 * @see "https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Forwarded" 518 * @see "https://ipcisco.com/lesson/ipv6-address/" 519 * @see "https://www.juniper.net/documentation/us/en/software/junos/interfaces-security-devices/topics/topic-map/security-interface-ipv4-ipv6-protocol.html" 520 * @see "https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/net/InetAddress.html#getByName(java.lang.String)" 521 * @see "https://www.arin.net/reference/research/statistics/address_filters/" 522 * @see "https://en.wikipedia.org/wiki/Multicast_address" 523 * @see "https://stackoverflow.com/a/5619409" 524 * @see "https://www.ripe.net/media/documents/ipv6-address-types.pdf" 525 * @see "https://www.iana.org/assignments/ipv6-unicast-address-assignments/ipv6-unicast-address-assignments.xhtml" 526 * @see "https://developer.android.com/reference/java/net/Inet6Address" 527 * @see "https://en.wikipedia.org/wiki/Unique_local_address" 528 */ 529 public static boolean isPublicIPAddress(String ip) { 530 boolean isValid = false; 531 try { 532 //Quick validation on the string itself based on characters used to compose an IP v4/v6 address 533 if (Pattern.matches("[0-9a-fA-F:.]+", ip)) { 534 //If OK then use the dedicated InetAddressValidator from Apache Commons Validator 535 if (InetAddressValidator.getInstance().isValid(ip)) { 536 //If OK then validate that is an public IP address 537 //From Javadoc for "InetAddress.getByName": If a literal IP address is supplied, only the validity of the address format is checked. 538 InetAddress addr = InetAddress.getByName(ip); 539 isValid = (!addr.isAnyLocalAddress() && !addr.isLinkLocalAddress() && !addr.isLoopbackAddress() && !addr.isMulticastAddress() && !addr.isSiteLocalAddress()); 540 //If OK and the IP is an V6 one then make additional validation because the built-in Java API will let pass some V6 IP 541 //For the prefix map, the start of the key indicates if the value is a regex or a string 542 if (isValid && (addr instanceof Inet6Address)) { 543 Map<String, String> prefixes = new HashMap<>(); 544 prefixes.put("REGEX_LOOPBACK", "^(0|:)+1$"); 545 prefixes.put("REGEX_UNIQUE-LOCAL-ADDRESSES", "^f(c|d)[a-f0-9]{2}:.*$"); 546 prefixes.put("STRING_LINK-LOCAL-ADDRESSES", "fe80:"); 547 prefixes.put("REGEX_TEREDO", "^2001:[0]*:.*$"); 548 prefixes.put("REGEX_BENCHMARKING", "^2001:[0]*2:.*$"); 549 prefixes.put("REGEX_ORCHID", "^2001:[0]*10:.*$"); 550 prefixes.put("STRING_DOCUMENTATION", "2001:db8:"); 551 prefixes.put("STRING_GLOBAL-UNICAST", "2000:"); 552 prefixes.put("REGEX_MULTICAST", "^ff[0-9]{2}:.*$"); 553 final List<Boolean> results = new ArrayList<>(); 554 final String ipLower = ip.trim().toLowerCase(Locale.ROOT); 555 prefixes.forEach((addressType, expr) -> { 556 String exprLower = expr.trim().toLowerCase(); 557 if (addressType.startsWith("STRING_")) { 558 results.add(ipLower.startsWith(exprLower)); 559 } else { 560 results.add(Pattern.matches(exprLower, ipLower)); 561 } 562 }); 563 isValid = ((results.size() == prefixes.size()) && !results.contains(Boolean.TRUE)); 564 } 565 } 566 } 567 } catch (Exception e) { 568 isValid = false; 569 } 570 return isValid; 571 } 572 573 /** 574 * Compute a SHA256 hash from an input composed of a collection of strings.<br><br> 575 * This method take care to build the source string in a way to prevent this source string to be prone to abuse targeting the different parts composing it.<br><br> 576 * <p> 577 * Example of possible abuse without precautions applied during the hash calculation logic:<br> 578 * Hash of <code>SHA256("Hello", "My", "World!!!")</code> will be equals to the hash of <code>SHA256("Hell", "oMyW", "orld!!!")</code>.<br> 579 * </p> 580 * This method ensure that both hash above will be different.<br><br> 581 * 582 * <b>Note:</b> The character <code>|</code> is used, as separator, of every parts so a part is not allowed to contains this character. 583 * 584 * @param parts Ordered list of strings to use to build the input string for which the hash must be computed on. No null value is accepted on object composing the collection. 585 * @return The hash, as an array of bytes, to allow caller to convert it to the final representation wanted (HEX, Base64, etc.). If the collection passed is null or empty then the method return null. 586 * @throws Exception If any exception occurs 587 * @see "https://github.com/righettod/code-snippets-security-utils/issues/16" 588 * @see "https://pentesterlab.com/badges/codereview" 589 * @see "https://blog.trailofbits.com/2024/08/21/yolo-is-not-a-valid-hash-construction/" 590 * @see "https://www.nist.gov/publications/sha-3-derived-functions-cshake-kmac-tuplehash-and-parallelhash" 591 */ 592 public static byte[] computeHashNoProneToAbuseOnParts(List<String> parts) throws Exception { 593 byte[] hash = null; 594 String separator = "|"; 595 if (parts != null && !parts.isEmpty()) { 596 //Ensure that not part is null 597 if (parts.stream().anyMatch(Objects::isNull)) { 598 throw new IllegalArgumentException("No part must be null!"); 599 } 600 //Ensure that the separator is absent from every part 601 if (parts.stream().anyMatch(part -> part.contains(separator))) { 602 throw new IllegalArgumentException(String.format("The character '%s', used as parts separator, must be absent from every parts!", separator)); 603 } 604 MessageDigest digest = MessageDigest.getInstance("SHA-256"); 605 final StringBuilder buffer = new StringBuilder(separator); 606 parts.forEach(p -> { 607 buffer.append(p).append(separator); 608 }); 609 hash = digest.digest(buffer.toString().getBytes(StandardCharsets.UTF_8)); 610 } 611 return hash; 612 } 613 614 /** 615 * Ensure that an XML file only uses DTD/XSD references (called System Identifier) present in the allowed list provided.<br><br> 616 * The code is based on the validation implemented into the OpenJDK 21, by the class <b><a href="https://github.com/openjdk/jdk/blob/jdk-21%2B35/src/java.prefs/share/classes/java/util/prefs/XmlSupport.java">java.util.prefs.XmlSupport</a></b>, in the method <b><a href="https://github.com/openjdk/jdk/blob/jdk-21%2B35/src/java.prefs/share/classes/java/util/prefs/XmlSupport.java#L240">loadPrefsDoc()</a></b>.<br><br> 617 * The method also ensure that no Public Identifier is used to prevent potential bypasses of the validations. 618 * 619 * @param xmlFilePath Filename of the XML file to check. 620 * @param allowedSystemIdentifiers List of URL allowed for System Identifier specified for any XSD/DTD references. 621 * @return True only if the file pass all validations. 622 * @see "https://www.w3schools.com/xml/prop_documenttype_systemid.asp" 623 * @see "https://www.ibm.com/docs/en/integration-bus/9.0.0?topic=doctypedecl-xml-systemid" 624 * @see "https://www.liquid-technologies.com/Reference/Glossary/XML_DocType.html" 625 * @see "https://www.xml.com/pub/98/08/xmlqna0.html" 626 * @see "https://github.com/openjdk/jdk/blob/jdk-21%2B35/src/java.prefs/share/classes/java/util/prefs/XmlSupport.java#L397" 627 * @see "https://en.wikipedia.org/wiki/Formal_Public_Identifier" 628 */ 629 public static boolean isXMLOnlyUseAllowedXSDorDTD(String xmlFilePath, final List<String> allowedSystemIdentifiers) { 630 boolean isSafe = false; 631 final String errorTemplate = "Non allowed %s ID detected!"; 632 final String emptyFakeDTD = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><!ELEMENT dummy EMPTY>"; 633 final String emptyFakeXSD = "<xs:schema xmlns:xs=\"http://www.w3.org/2001/XMLSchema\"> <xs:element name=\"dummy\"/></xs:schema>"; 634 635 if (allowedSystemIdentifiers == null || allowedSystemIdentifiers.isEmpty()) { 636 throw new IllegalArgumentException("At least one SID must be specified!"); 637 } 638 File xmlFile = new File(xmlFilePath); 639 if (xmlFile.exists() && xmlFile.canRead() && xmlFile.isFile()) { 640 try { 641 EntityResolver resolverValidator = (publicId, systemId) -> { 642 if (publicId != null) { 643 throw new SAXException(String.format(errorTemplate, "PUBLIC")); 644 } 645 if (!allowedSystemIdentifiers.contains(systemId)) { 646 throw new SAXException(String.format(errorTemplate, "SYSTEM")); 647 } 648 //If it is OK then return a empty DTD/XSD 649 return new InputSource(new StringReader(systemId.toLowerCase().endsWith(".dtd") ? emptyFakeDTD : emptyFakeXSD)); 650 }; 651 DocumentBuilderFactory dbfInstance = DocumentBuilderFactory.newInstance(); 652 dbfInstance.setIgnoringElementContentWhitespace(true); 653 dbfInstance.setXIncludeAware(false); 654 dbfInstance.setValidating(false); 655 dbfInstance.setCoalescing(true); 656 dbfInstance.setIgnoringComments(false); 657 DocumentBuilder builder = dbfInstance.newDocumentBuilder(); 658 builder.setEntityResolver(resolverValidator); 659 Document doc = builder.parse(xmlFile); 660 isSafe = (doc != null); 661 } catch (SAXException | IOException | ParserConfigurationException e) { 662 isSafe = false; 663 } 664 } 665 666 return isSafe; 667 } 668 669 /** 670 * Apply a collection of validations on a EXCEL CSV file provided (file was expected to be opened in Microsoft EXCEL): 671 * <ul> 672 * <li>Real CSV file.</li> 673 * <li>Do not contains any payload related to a CSV injections.</li> 674 * </ul> 675 * Ensure that, if Apache Commons CSV does not find any record then, the file will be considered as NOT safe (prevent potential bypasses).<br><br> 676 * <b>Note:</b> Record delimiter used is the <code>,</code> (comma) character. See the Apache Commons CSV reference provided for EXCEL.<br> 677 * 678 * @param csvFilePath Filename of the CSV file to check. 679 * @return True only if the file pass all validations. 680 * @see "https://commons.apache.org/proper/commons-csv/" 681 * @see "https://commons.apache.org/proper/commons-csv/apidocs/org/apache/commons/csv/CSVFormat.html#EXCEL" 682 * @see "https://www.we45.com/post/your-excel-sheets-are-not-safe-heres-how-to-beat-csv-injection" 683 * @see "https://www.whiteoaksecurity.com/blog/2020-4-23-csv-injection-whats-the-risk/" 684 * @see "https://book.hacktricks.xyz/pentesting-web/formula-csv-doc-latex-ghostscript-injection" 685 * @see "https://owasp.org/www-community/attacks/CSV_Injection" 686 * @see "https://payatu.com/blog/csv-injection-basic-to-exploit/" 687 * @see "https://cwe.mitre.org/data/definitions/1236.html" 688 */ 689 public static boolean isExcelCSVSafe(String csvFilePath) { 690 boolean isSafe; 691 final AtomicInteger recordCount = new AtomicInteger(); 692 final List<Character> payloadDetectionCharacters = List.of('=', '+', '@', '-', '\r', '\t'); 693 694 try { 695 final List<String> payloadsIdentified = new ArrayList<>(); 696 try (Reader in = new FileReader(csvFilePath)) { 697 Iterable<CSVRecord> records = CSVFormat.EXCEL.parse(in); 698 records.forEach(record -> { 699 record.forEach(recordValue -> { 700 if (recordValue != null && !recordValue.trim().isEmpty() && payloadDetectionCharacters.contains(recordValue.trim().charAt(0))) { 701 payloadsIdentified.add(recordValue); 702 } 703 recordCount.getAndIncrement(); 704 }); 705 }); 706 } 707 isSafe = (payloadsIdentified.isEmpty() && recordCount.get() > 0); 708 } catch (Exception e) { 709 isSafe = false; 710 } 711 712 return isSafe; 713 } 714 715 /** 716 * Provide a way to add an integrity marker (<a href="https://en.wikipedia.org/wiki/HMAC">HMAC</a>) to a serialized object serialized using the <a href="https://www.baeldung.com/java-serialization">java native system</a> (binary).<br> 717 * The goal is to provide <b>a temporary workaround</b> to try to prevent deserialization attacks and give time to move to a text-based serialization approach. 718 * 719 * @param processingMode Define the mode of processing i.e. protect or validate. ({@link eu.righettod.ProcessingMode}) 720 * @param input When the processing mode is "protect" than the expected input (string) is a java serialized object encoded in Base64 otherwise (processing mode is "validate") expected input is the output of this method when the "protect" mode was used. 721 * @param secret Secret to use to compute the SHA256 HMAC. 722 * @return A map with the following keys: <ul><li><b>PROCESSING_MODE</b>: Processing mode used to compute the result.</li><li><b>STATUS</b>: A boolean indicating if the processing was successful or not.</li><li><b>RESULT</b>: Always contains a string representing the protected serialized object in the format <code>[SERIALIZED_OBJECT_BASE64_ENCODED]:[SERIALIZED_OBJECT_HMAC_BASE64_ENCODED]</code>.</li></ul> 723 * @throws Exception If any exception occurs. 724 * @see "https://cheatsheetseries.owasp.org/cheatsheets/Deserialization_Cheat_Sheet.html" 725 * @see "https://owasp.org/www-project-top-ten/2017/A8_2017-Insecure_Deserialization" 726 * @see "https://portswigger.net/web-security/deserialization" 727 * @see "https://www.baeldung.com/java-serialization-approaches" 728 * @see "https://www.baeldung.com/java-serialization" 729 * @see "https://cryptobook.nakov.com/mac-and-key-derivation/hmac-and-key-derivation" 730 * @see "https://en.wikipedia.org/wiki/HMAC" 731 * @see "https://smattme.com/posts/how-to-generate-hmac-signature-in-java/" 732 */ 733 public static Map<String, Object> ensureSerializedObjectIntegrity(ProcessingMode processingMode, String input, byte[] secret) throws Exception { 734 Map<String, Object> results; 735 String resultFormatTemplate = "%s:%s"; 736 //Verify input provided to be consistent 737 if (processingMode == null) { 738 throw new IllegalArgumentException("The processing mode is mandatory!"); 739 } 740 if (input == null || input.trim().isEmpty()) { 741 throw new IllegalArgumentException("Input data is mandatory!"); 742 } 743 if (secret == null || secret.length == 0) { 744 throw new IllegalArgumentException("The HMAC secret is mandatory!"); 745 } 746 if (processingMode.equals(ProcessingMode.VALIDATE) && input.split(":").length != 2) { 747 throw new IllegalArgumentException("Input data provided is invalid for the processing mode specified!"); 748 } 749 //Processing 750 Base64.Decoder b64Decoder = Base64.getDecoder(); 751 Base64.Encoder b64Encoder = Base64.getEncoder(); 752 String hmacAlgorithm = "HmacSHA256"; 753 Mac mac = Mac.getInstance(hmacAlgorithm); 754 SecretKeySpec key = new SecretKeySpec(secret, hmacAlgorithm); 755 mac.init(key); 756 results = new HashMap<>(); 757 results.put("PROCESSING_MODE", processingMode.toString()); 758 switch (processingMode) { 759 case PROTECT -> { 760 byte[] objectBytes = b64Decoder.decode(input); 761 byte[] hmac = mac.doFinal(objectBytes); 762 String encodedHmac = b64Encoder.encodeToString(hmac); 763 results.put("STATUS", Boolean.TRUE); 764 results.put("RESULT", String.format(resultFormatTemplate, input, encodedHmac)); 765 } 766 case VALIDATE -> { 767 String[] parts = input.split(":"); 768 byte[] objectBytes = b64Decoder.decode(parts[0].trim()); 769 byte[] hmacProvided = b64Decoder.decode(parts[1].trim()); 770 byte[] hmacComputed = mac.doFinal(objectBytes); 771 String encodedHmacComputed = b64Encoder.encodeToString(hmacComputed); 772 Boolean hmacIsValid = Arrays.equals(hmacProvided, hmacComputed); 773 results.put("STATUS", hmacIsValid); 774 results.put("RESULT", String.format(resultFormatTemplate, parts[0].trim(), encodedHmacComputed)); 775 } 776 default -> throw new IllegalArgumentException("Not supported processing mode!"); 777 } 778 return results; 779 } 780 781 /** 782 * Apply a collection of validations on a JSON string provided: 783 * <ul> 784 * <li>Real JSON structure.</li> 785 * <li>Contain less than a specified number of deepness for nested objects or arrays.</li> 786 * <li>Contain less than a specified number of items in any arrays.</li> 787 * </ul> 788 * <br> 789 * <b>Note:</b> I decided to use a parsing approach using only string processing to prevent any StackOverFlow or OutOfMemory error that can be abused.<br><br> 790 * I used the following assumption: 791 * <ul> 792 * <li>The character <code>{</code> identify the beginning of an object.</li> 793 * <li>The character <code>}</code> identify the end of an object.</li> 794 * <li>The character <code>[</code> identify the beginning of an array.</li> 795 * <li>The character <code>]</code> identify the end of an array.</li> 796 * <li>The character <code>"</code> identify the delimiter of a string.</li> 797 * <li>The character sequence <code>\"</code> identify the escaping of an double quote.</li> 798 * </ul> 799 * 800 * @param json String containing the JSON data to validate. 801 * @param maxItemsByArraysCount Maximum number of items allowed in an array. 802 * @param maxDeepnessAllowed Maximum number nested objects or arrays allowed. 803 * @return True only if the string pass all validations. 804 * @see "https://javaee.github.io/jsonp/" 805 * @see "https://community.f5.com/discussions/technicalforum/disable-buffer-overflow-in-json-parameters/124306" 806 * @see "https://github.com/InductiveComputerScience/pbJson/issues/2" 807 */ 808 public static boolean isJSONSafe(String json, int maxItemsByArraysCount, int maxDeepnessAllowed) { 809 boolean isSafe = false; 810 811 try { 812 //Step 1: Analyse the JSON string 813 int currentDeepness = 0; 814 int currentArrayItemsCount = 0; 815 int maxDeepnessReached = 0; 816 int maxArrayItemsCountReached = 0; 817 boolean currentlyInArray = false; 818 boolean currentlyInString = false; 819 int currentNestedArrayLevel = 0; 820 String jsonEscapedDoubleQuote = "\\\"";//Escaped double quote must not be considered as a string delimiter 821 String work = json.replace(jsonEscapedDoubleQuote, "'"); 822 for (char c : work.toCharArray()) { 823 switch (c) { 824 case '{': { 825 if (!currentlyInString) { 826 currentDeepness++; 827 } 828 break; 829 } 830 case '}': { 831 if (!currentlyInString) { 832 currentDeepness--; 833 } 834 break; 835 } 836 case '[': { 837 if (!currentlyInString) { 838 currentDeepness++; 839 if (currentlyInArray) { 840 currentNestedArrayLevel++; 841 } 842 currentlyInArray = true; 843 } 844 break; 845 } 846 case ']': { 847 if (!currentlyInString) { 848 currentDeepness--; 849 currentArrayItemsCount = 0; 850 if (currentNestedArrayLevel > 0) { 851 currentNestedArrayLevel--; 852 } 853 if (currentNestedArrayLevel == 0) { 854 currentlyInArray = false; 855 } 856 } 857 break; 858 } 859 case '"': { 860 currentlyInString = !currentlyInString; 861 break; 862 } 863 case ',': { 864 if (!currentlyInString && currentlyInArray) { 865 currentArrayItemsCount++; 866 } 867 break; 868 } 869 } 870 if (currentDeepness > maxDeepnessReached) { 871 maxDeepnessReached = currentDeepness; 872 } 873 if (currentArrayItemsCount > maxArrayItemsCountReached) { 874 maxArrayItemsCountReached = currentArrayItemsCount; 875 } 876 } 877 //Step 2: Apply validation against the value specified as limits 878 isSafe = ((maxItemsByArraysCount > maxArrayItemsCountReached) && (maxDeepnessAllowed > maxDeepnessReached)); 879 880 //Step 3: If the content is safe then ensure that it is valid JSON structure using the "Java API for JSON Processing" (JSR 374) parser reference implementation. 881 if (isSafe) { 882 JsonReader reader = Json.createReader(new StringReader(json)); 883 isSafe = (reader.read() != null); 884 } 885 886 } catch (Exception e) { 887 isSafe = false; 888 } 889 return isSafe; 890 } 891 892 /** 893 * Apply a collection of validations on a image file provided: 894 * <ul> 895 * <li>Real image file.</li> 896 * <li>Its mime type is into the list of allowed mime types.</li> 897 * <li>Its metadata fields do not contains any characters related to a malicious payloads.</li> 898 * </ul> 899 * <br> 900 * <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. 901 * 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> 902 * 903 * @param imageFilePath Filename of the image file to check. 904 * @param imageAllowedMimeTypes List of image mime types allowed. 905 * @return True only if the file pass all validations. 906 * @see "https://commons.apache.org/proper/commons-imaging/" 907 * @see "https://commons.apache.org/proper/commons-imaging/formatsupport.html" 908 * @see "https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types" 909 * @see "https://www.iana.org/assignments/media-types/media-types.xhtml#image" 910 * @see "https://www.synacktiv.com/en/publications/persistent-php-payloads-in-pngs-how-to-inject-php-code-in-an-image-and-keep-it-there" 911 * @see "https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html" 912 * @see "https://github.com/righettod/document-upload-protection/blob/master/src/main/java/eu/righettod/poc/sanitizer/ImageDocumentSanitizerImpl.java" 913 * @see "https://exiftool.org/examples.html" 914 * @see "https://en.wikipedia.org/wiki/List_of_file_signatures" 915 * @see "https://hexed.it/" 916 * @see "https://github.com/sighook/pixload" 917 */ 918 public static boolean isImageSafe(String imageFilePath, List<String> imageAllowedMimeTypes) { 919 boolean isSafe = false; 920 Pattern payloadDetectionRegex = Pattern.compile("[<>${}`]+", Pattern.CASE_INSENSITIVE); 921 try { 922 File imgFile = new File(imageFilePath); 923 if (imgFile.exists() && imgFile.canRead() && imgFile.isFile() && !imageAllowedMimeTypes.isEmpty()) { 924 final byte[] imgBytes = Files.readAllBytes(imgFile.toPath()); 925 //Step 1: Check the mime type of the file against the allowed ones 926 ImageInfo imgInfo = Imaging.getImageInfo(imgBytes); 927 if (imageAllowedMimeTypes.contains(imgInfo.getMimeType())) { 928 //Step 2: Load the image into an object using the Image API 929 BufferedImage imgObject = Imaging.getBufferedImage(imgBytes); 930 if (imgObject != null && imgObject.getWidth() > 0 && imgObject.getHeight() > 0) { 931 //Step 3: Check the metadata if the image format support it - Highly experimental 932 List<String> metadataWithPayloads = new ArrayList<>(); 933 final ImageMetadata imgMetadata = Imaging.getMetadata(imgBytes); 934 if (imgMetadata != null) { 935 imgMetadata.getItems().forEach(item -> { 936 String metadata = item.toString(); 937 if (payloadDetectionRegex.matcher(metadata).find()) { 938 metadataWithPayloads.add(metadata); 939 } 940 }); 941 } 942 isSafe = metadataWithPayloads.isEmpty(); 943 } 944 } 945 } 946 } catch (Exception e) { 947 isSafe = false; 948 } 949 return isSafe; 950 } 951 952 /** 953 * Rewrite the input file to remove any embedded files that is not embedded using a methods supported by the official format of the file.<br> 954 * Example: a file can be embedded by adding it to the end of the source file, see the reference provided for details. 955 * 956 * @param inputFilePath Filename of the file to clean up. 957 * @param inputFileType Type of the file provided. 958 * @return A array of bytes with the cleaned file. 959 * @throws IllegalArgumentException If an invalid parameter is passed 960 * @throws Exception If any technical error during the cleaning processing 961 * @see "https://www.synacktiv.com/en/publications/persistent-php-payloads-in-pngs-how-to-inject-php-code-in-an-image-and-keep-it-there" 962 * @see "https://github.com/righettod/toolbox-pentest-web/tree/master/misc" 963 * @see "https://github.com/righettod/toolbox-pentest-web?tab=readme-ov-file#misc" 964 * @see "https://stackoverflow.com/a/13605411" 965 */ 966 public static byte[] sanitizeFile(String inputFilePath, InputFileType inputFileType) throws Exception { 967 ByteArrayOutputStream sanitizedContent = new ByteArrayOutputStream(); 968 File inputFile = new File(inputFilePath); 969 if (!inputFile.exists() || !inputFile.canRead() || !inputFile.isFile()) { 970 throw new IllegalArgumentException("Cannot read the content of the input file!"); 971 } 972 switch (inputFileType) { 973 case PDF -> { 974 try (PDDocument document = Loader.loadPDF(inputFile)) { 975 document.save(sanitizedContent); 976 } 977 } 978 case IMAGE -> { 979 // Load the original image 980 BufferedImage originalImage = ImageIO.read(inputFile); 981 String originalFormat = identifyMimeType(Files.readAllBytes(inputFile.toPath())).split("/")[1].trim(); 982 // Check that image has been successfully loaded 983 if (originalImage == null) { 984 throw new IOException("Cannot load the original image !"); 985 } 986 // Get current Width and Height of the image 987 int originalWidth = originalImage.getWidth(null); 988 int originalHeight = originalImage.getHeight(null); 989 // Resize the image by removing 1px on Width and Height 990 Image resizedImage = originalImage.getScaledInstance(originalWidth - 1, originalHeight - 1, Image.SCALE_SMOOTH); 991 // Resize the resized image by adding 1px on Width and Height - In fact set image to is initial size 992 Image initialSizedImage = resizedImage.getScaledInstance(originalWidth, originalHeight, Image.SCALE_SMOOTH); 993 // Save image to a bytes buffer 994 int bufferedImageType = BufferedImage.TYPE_INT_ARGB;//By default use a format supporting transparency 995 if ("jpeg".equalsIgnoreCase(originalFormat) || "bmp".equalsIgnoreCase(originalFormat)) { 996 bufferedImageType = BufferedImage.TYPE_INT_RGB; 997 } 998 BufferedImage sanitizedImage = new BufferedImage(initialSizedImage.getWidth(null), initialSizedImage.getHeight(null), bufferedImageType); 999 Graphics2D drawer = sanitizedImage.createGraphics(); 1000 drawer.drawImage(initialSizedImage, 0, 0, null); 1001 drawer.dispose(); 1002 ImageIO.write(sanitizedImage, originalFormat, sanitizedContent); 1003 } 1004 default -> throw new IllegalArgumentException("Type of file not supported !"); 1005 } 1006 if (sanitizedContent.size() == 0) { 1007 throw new IOException("An error occur during the rewrite operation!"); 1008 } 1009 return sanitizedContent.toByteArray(); 1010 } 1011 1012 /** 1013 * Apply a collection of validations on a string expected to be an email address: 1014 * <ul> 1015 * <li>Is a valid email address, from a parser perspective, following RFCs on email addresses.</li> 1016 * <li>Is not using "Encoded-word" format.</li> 1017 * <li>Is not using comment format.</li> 1018 * <li>Is not using "Punycode" format.</li> 1019 * <li>Is not using UUCP style addresses.</li> 1020 * <li>Is not using address literals.</li> 1021 * <li>Is not using source routes.</li> 1022 * <li>Is not using the "percent hack".</li> 1023 * </ul><br> 1024 * This is based on the research work from <a href="https://portswigger.net/research/gareth-heyes">Gareth Heyes</a> added in references (Portswigger).<br><br> 1025 * 1026 * <b>Note:</b> The notion of valid, here, is to take from a secure usage of the data perspective. 1027 * 1028 * @param addr String expected to be a valid email address. 1029 * @return True only if the string pass all validations. 1030 * @see "https://commons.apache.org/proper/commons-validator/" 1031 * @see "https://commons.apache.org/proper/commons-validator/apidocs/org/apache/commons/validator/routines/EmailValidator.html" 1032 * @see "https://datatracker.ietf.org/doc/html/rfc2047#section-2" 1033 * @see "https://portswigger.net/research/splitting-the-email-atom" 1034 * @see "https://www.jochentopf.com/email/address.html" 1035 * @see "https://en.wikipedia.org/wiki/Email_address" 1036 */ 1037 public static boolean isEmailAddress(String addr) { 1038 boolean isValid = false; 1039 String work = addr.toLowerCase(Locale.ROOT); 1040 Pattern encodedWordRegex = Pattern.compile("[=?]+", Pattern.CASE_INSENSITIVE); 1041 Pattern forbiddenCharacterRegex = Pattern.compile("[():!%\\[\\],;]+", Pattern.CASE_INSENSITIVE); 1042 try { 1043 //Start with the use of the dedicated EmailValidator from Apache Commons Validator 1044 if (EmailValidator.getInstance(true, true).isValid(work)) { 1045 //If OK then validate it does not contains "Encoded-word" patterns using an aggressive approach 1046 if (!encodedWordRegex.matcher(work).find()) { 1047 //If OK then validate it does not contains punycode 1048 if (!work.contains("xn--")) { 1049 //If OK then validate it does not use: 1050 // UUCP style addresses, 1051 // Comment format, 1052 // Address literals, 1053 // Source routes, 1054 // The percent hack. 1055 if (!forbiddenCharacterRegex.matcher(work).find()) { 1056 isValid = true; 1057 } 1058 } 1059 } 1060 } 1061 } catch (Exception e) { 1062 isValid = false; 1063 } 1064 return isValid; 1065 } 1066 1067 /** 1068 * The <a href="https://www.stet.eu/en/psd2/">PSD2 STET</a> specification require to use <a href="https://datatracker.ietf.org/doc/draft-cavage-http-signatures/">HTTP Signature</a>. 1069 * <br> 1070 * Section <b>3.5.1.2</b> of the document <a href="https://www.stet.eu/assets/files/PSD2/1-6-3/api-dsp2-stet-v1.6.3.1-part-1-framework.pdf">Documentation Framework</a> version <b>1.6.3</b>. 1071 * <br> 1072 * The problem is that, by design, the HTTP Signature specification is prone to blind SSRF. 1073 * <br> 1074 * URL example taken from the STET specification: <code>https://path.to/myQsealCertificate_714f8154ec259ac40b8a9786c9908488b2582b68b17e865fede4636d726b709f</code>. 1075 * <br> 1076 * The objective of this code is to try to decrease the "exploitability/interest" of this SSRF for an attacker. 1077 * 1078 * @param certificateUrl Url pointing to a Qualified Certificate (QSealC) encoded in PEM format and respecting the ETSI/TS119495 technical Specification . 1079 * @return TRUE only if the url point to a Qualified Certificate in PEM format. 1080 * @see "https://www.stet.eu/en/psd2/" 1081 * @see "https://www.stet.eu/assets/files/PSD2/1-6-3/api-dsp2-stet-v1.6.3.1-part-1-framework.pdf" 1082 * @see "https://datatracker.ietf.org/doc/draft-cavage-http-signatures/" 1083 * @see "https://datatracker.ietf.org/doc/rfc9421/" 1084 * @see "https://openjdk.org/groups/net/httpclient/intro.html" 1085 * @see "https://docs.oracle.com/en/java/javase/21/docs/api/java.net.http/java/net/http/package-summary.html" 1086 * @see "https://portswigger.net/web-security/ssrf" 1087 * @see "https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control" 1088 */ 1089 public static boolean isPSD2StetSafeCertificateURL(String certificateUrl) { 1090 boolean isValid = false; 1091 long connectionTimeoutInSeconds = 10; 1092 String userAgent = "PSD2-STET-HTTPSignature-CertificateRequest"; 1093 try { 1094 //1. Ensure that the URL end with the SHA-256 fingerprint encoded in HEX of the certificate like requested by STET 1095 if (certificateUrl != null && certificateUrl.lastIndexOf("_") != -1) { 1096 String digestPart = certificateUrl.substring(certificateUrl.lastIndexOf("_") + 1); 1097 if (Pattern.matches("^[0-9a-f]{64}$", digestPart)) { 1098 //2. Ensure that the URL is a valid url by creating a instance of the class URI 1099 URI uri = URI.create(certificateUrl); 1100 //3. Require usage of HTTPS and reject any url containing query parameters 1101 if ("https".equalsIgnoreCase(uri.getScheme()) && uri.getQuery() == null) { 1102 //4. Perform a HTTP HEAD request in order to get the content type of the remote resource 1103 //and limit the interest to use the SSRF because to pass the check the url need to: 1104 //- Do not having any query parameters. 1105 //- Use HTTPS protocol. 1106 //- End with a string having the format "_[0-9a-f]{64}". 1107 //- Trigger the malicious action that the attacker want but with a HTTP HEAD without any redirect and parameters. 1108 HttpResponse<String> response; 1109 try (HttpClient client = HttpClient.newBuilder().followRedirects(HttpClient.Redirect.NEVER).build()) { 1110 HttpRequest request = HttpRequest.newBuilder().uri(uri).timeout(Duration.ofSeconds(connectionTimeoutInSeconds)).method("HEAD", HttpRequest.BodyPublishers.noBody()).header("User-Agent", userAgent)//To provide an hint to the target about the initiator of the request 1111 .header("Cache-Control", "no-store, max-age=0")//To prevent caching issues or abuses 1112 .build(); 1113 response = client.send(request, HttpResponse.BodyHandlers.ofString()); 1114 if (response.statusCode() == 200) { 1115 //5. Ensure that the response content type is "text/plain" 1116 Optional<String> contentType = response.headers().firstValue("Content-Type"); 1117 isValid = (contentType.isPresent() && contentType.get().trim().toLowerCase(Locale.ENGLISH).startsWith("text/plain")); 1118 } 1119 } 1120 } 1121 } 1122 } 1123 } catch (Exception e) { 1124 isValid = false; 1125 } 1126 return isValid; 1127 } 1128 1129 /** 1130 * Perform sequential URL decoding operations against a URL encoded data until the data is not URL encoded anymore or if the specified threshold is reached. 1131 * 1132 * @param encodedData URL encoded data. 1133 * @param decodingRoundThreshold Threshold above which decoding will fail. 1134 * @return The decoded data. 1135 * @throws SecurityException If the threshold is reached. 1136 * @see "https://en.wikipedia.org/wiki/Percent-encoding" 1137 * @see "https://owasp.org/www-community/Double_Encoding" 1138 * @see "https://portswigger.net/web-security/essential-skills/obfuscating-attacks-using-encodings" 1139 * @see "https://capec.mitre.org/data/definitions/120.html" 1140 */ 1141 public static String applyURLDecoding(String encodedData, int decodingRoundThreshold) throws SecurityException { 1142 if (decodingRoundThreshold < 1) { 1143 throw new IllegalArgumentException("Threshold must be a positive number !"); 1144 } 1145 if (encodedData == null) { 1146 throw new IllegalArgumentException("Data provided must not be null !"); 1147 } 1148 Charset charset = StandardCharsets.UTF_8; 1149 int currentDecodingRound = 0; 1150 boolean isFinished = false; 1151 String currentRoundData = encodedData; 1152 String previousRoundData = encodedData; 1153 while (!isFinished) { 1154 if (currentDecodingRound > decodingRoundThreshold) { 1155 throw new SecurityException(String.format("Decoding round threshold of %s reached!", decodingRoundThreshold)); 1156 } 1157 currentRoundData = URLDecoder.decode(currentRoundData, charset); 1158 isFinished = currentRoundData.equals(previousRoundData); 1159 previousRoundData = currentRoundData; 1160 currentDecodingRound++; 1161 } 1162 return currentRoundData; 1163 } 1164 1165 /** 1166 * Apply a collection of validations on a string expected to be an system file/folder path: 1167 * <ul> 1168 * <li>Does not contains path traversal payload.</li> 1169 * <li>The canonical path is equals to the absolute path.</li> 1170 * </ul><br> 1171 * 1172 * @param path String expected to be a valid system file/folder path. 1173 * @return True only if the string pass all validations. 1174 * @see "https://portswigger.net/web-security/file-path-traversal" 1175 * @see "https://learn.snyk.io/lesson/directory-traversal/" 1176 * @see "https://capec.mitre.org/data/definitions/126.html" 1177 * @see "https://owasp.org/www-community/attacks/Path_Traversal" 1178 */ 1179 public static boolean isPathSafe(String path) { 1180 boolean isSafe = false; 1181 int decodingRoundThreshold = 3; 1182 try { 1183 if (path != null && !path.isEmpty()) { 1184 //URL decode the path if case of data coming from a web context 1185 String decodedPath = applyURLDecoding(path, decodingRoundThreshold); 1186 //Ensure that no path traversal expression is present 1187 if (!decodedPath.contains("..")) { 1188 File f = new File(decodedPath); 1189 String canonicalPath = f.getCanonicalPath(); 1190 String absolutePath = f.getAbsolutePath(); 1191 isSafe = canonicalPath.equals(absolutePath); 1192 } 1193 } 1194 } catch (Exception e) { 1195 isSafe = false; 1196 } 1197 return isSafe; 1198 } 1199 1200 /** 1201 * Identify if an XML contains any XML comments or have any XSL processing instructions.<br> 1202 * Stream reader based parsing is used to support large XML tree. 1203 * 1204 * @param xmlFilePath Filename of the XML file to check. 1205 * @return True only if XML comments or XSL processing instructions are identified. 1206 * @see "https://www.tutorialspoint.com/xml/xml_processing.htm" 1207 * @see "https://docs.oracle.com/en/java/javase/21/docs/api/java.xml/javax/xml/stream/XMLInputFactory.html" 1208 * @see "https://portswigger.net/kb/issues/00400700_xml-entity-expansion" 1209 * @see "https://www.w3.org/Style/styling-XML.en.html" 1210 */ 1211 public static boolean isXMLHaveCommentsOrXSLProcessingInstructions(String xmlFilePath) { 1212 boolean itemsDetected = false; 1213 try { 1214 //Ensure that the parser will not be prone XML external entity (XXE) injection or XML entity expansion (XEE) attacks 1215 XMLInputFactory xmlInputFactory = XMLInputFactory.newFactory(); 1216 xmlInputFactory.setProperty(XMLInputFactory.SUPPORT_DTD, false); 1217 xmlInputFactory.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, ""); 1218 xmlInputFactory.setProperty(XMLInputFactory.IS_REPLACING_ENTITY_REFERENCES, false); 1219 xmlInputFactory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false); 1220 1221 //Parse file 1222 try (FileInputStream fis = new FileInputStream(xmlFilePath)) { 1223 XMLStreamReader reader = xmlInputFactory.createXMLStreamReader(fis); 1224 int eventType; 1225 while (reader.hasNext() && !itemsDetected) { 1226 eventType = reader.next(); 1227 if (eventType == XMLEvent.COMMENT) { 1228 itemsDetected = true; 1229 } else if (eventType == XMLEvent.PROCESSING_INSTRUCTION && "xml-stylesheet".equalsIgnoreCase(reader.getPITarget())) { 1230 itemsDetected = true; 1231 } 1232 } 1233 } 1234 } catch (Exception e) { 1235 //In case of error then assume that the check failed 1236 itemsDetected = true; 1237 } 1238 return itemsDetected; 1239 } 1240 1241 1242 /** 1243 * Perform a set of additional validations against a JWT token: 1244 * <ul> 1245 * <li>Do not use the <b>NONE</b> signature algorithm.</li> 1246 * <li>Have a <a href="https://www.iana.org/assignments/jwt/jwt.xhtml">EXP claim</a> defined.</li> 1247 * <li>The token identifier (<a href="https://www.iana.org/assignments/jwt/jwt.xhtml">JTI claim</a>) is NOT part of the list of revoked token.</li> 1248 * <li>Match the expected type of token: ACCESS or ID or REFRESH.</li> 1249 * </ul> 1250 * 1251 * @param token JWT token for which <b>signature was already validated</b> and on which a set of additional validations will be applied. 1252 * @param expectedTokenType The type of expected token using the enumeration provided. 1253 * @param revokedTokenJTIList A list of token identifier (<b>JTI</b> claim) referring to tokens that were revoked and to which the JTI claim of the token will be compared to. 1254 * @return True only the token pass all the validations. 1255 * @see "https://www.iana.org/assignments/jwt/jwt.xhtml" 1256 * @see "https://auth0.com/docs/secure/tokens/access-tokens" 1257 * @see "https://auth0.com/docs/secure/tokens/id-tokens" 1258 * @see "https://auth0.com/docs/secure/tokens/refresh-tokens" 1259 * @see "https://auth0.com/blog/id-token-access-token-what-is-the-difference/" 1260 * @see "https://jwt.io/libraries?language=Java" 1261 * @see "https://pentesterlab.com/blog/secure-jwt-library-design" 1262 * @see "https://github.com/auth0/java-jwt" 1263 */ 1264 public static boolean applyJWTExtraValidation(DecodedJWT token, TokenType expectedTokenType, List<String> revokedTokenJTIList) { 1265 boolean isValid = false; 1266 TokenType tokenType; 1267 try { 1268 if (!"none".equalsIgnoreCase(token.getAlgorithm().trim())) { 1269 if (!token.getClaim("exp").isMissing() && token.getExpiresAt() != null) { 1270 String jti = token.getId(); 1271 if (jti != null && !jti.trim().isEmpty()) { 1272 boolean jtiIsRevoked = revokedTokenJTIList.stream().anyMatch(jti::equalsIgnoreCase); 1273 if (!jtiIsRevoked) { 1274 //Determine the token type based on the presence of specifics claims 1275 if (!token.getClaim("scope").isMissing()) { 1276 tokenType = TokenType.ACCESS; 1277 } else if (!token.getClaim("name").isMissing() || !token.getClaim("email").isMissing()) { 1278 tokenType = TokenType.ID; 1279 } else { 1280 tokenType = TokenType.REFRESH; 1281 } 1282 isValid = (tokenType.equals(expectedTokenType)); 1283 } 1284 } 1285 } 1286 } 1287 1288 } catch (Exception e) { 1289 //In case of error then assume that the check failed 1290 isValid = false; 1291 } 1292 return isValid; 1293 } 1294 1295 /** 1296 * Apply a validations on a regular expression to ensure that is not prone to the ReDOS attack. 1297 * <br>If your technology is supported by <a href="https://github.com/doyensec/regexploit">regexploit</a> then <b>use it instead of this method!</b> 1298 * <br>Indeed, the <a href="https://www.doyensec.com/">Doyensec</a> team has made an intensive and amazing work on this topic and created this effective tool. 1299 * 1300 * @param regex String expected to be a valid regular expression (regex). 1301 * @param data Test data on which the regular expression is executed for the test. 1302 * @param maximumRunningTimeInSeconds Optional parameter to specify a number of seconds above which a regex execution time is considered as not safe (default to 4 seconds when not specified). 1303 * @return True only if the string pass all validations. 1304 * @see "https://github.blog/security/how-to-fix-a-redos/" 1305 * @see "https://learn.snyk.io/lesson/redos" 1306 * @see "https://rules.sonarsource.com/java/RSPEC-2631/" 1307 * @see "https://github.com/doyensec/regexploit" 1308 * @see "https://wiki.owasp.org/images/2/23/OWASP_IL_2009_ReDoS.pdf" 1309 * @see "https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS" 1310 */ 1311 public static boolean isRegexSafe(String regex, String data, Optional<Integer> maximumRunningTimeInSeconds) { 1312 Objects.requireNonNull(maximumRunningTimeInSeconds, "Use 'Optional.empty()' to leverage the default value."); 1313 Objects.requireNonNull(data, "A sample data is needed to perform the test."); 1314 Objects.requireNonNull(regex, "A regular expression is needed to perform the test."); 1315 boolean isSafe = false; 1316 int executionTimeout = maximumRunningTimeInSeconds.orElse(4); 1317 ExecutorService executor = Executors.newSingleThreadExecutor(); 1318 try { 1319 Callable<Boolean> task = () -> { 1320 Pattern pattern = Pattern.compile(regex); 1321 return pattern.matcher(data).matches(); 1322 }; 1323 List<Future<Boolean>> tasks = executor.invokeAll(List.of(task), executionTimeout, TimeUnit.SECONDS); 1324 if (!tasks.getFirst().isCancelled()) { 1325 isSafe = true; 1326 } 1327 } catch (Exception e) { 1328 isSafe = false; 1329 } finally { 1330 executor.shutdownNow(); 1331 } 1332 return isSafe; 1333 } 1334 1335 /** 1336 * Compute a UUID version 7 without using any external dependency.<br><br> 1337 * <b>Below are my personal point of view and perhaps I'm totally wrong!</b> 1338 * <br><br> 1339 * Why such method? 1340 * <ul> 1341 * <li>Java inferior or equals to 21 does not supports natively the generation of an UUID version 7.</li> 1342 * <li>Import a library just to generate such value is overkill for me.</li> 1343 * <li>Library that I have found, generating such version of an UUID, are not provided by entities commonly used in the java world, such as the SPRING framework provider.</li> 1344 * </ul> 1345 * <br> 1346 * <b>Full credits for this implementation goes to the authors and contributors of the <a href="https://github.com/nalgeon/uuidv7">UUIDv7</a> project.</b> 1347 * <br><br> 1348 * Below are the java libraries that I have found but, for which, I do not trust enough the provider to use them directly: 1349 * <ul> 1350 * <li><a href="https://github.com/cowtowncoder/java-uuid-generator">java-uuid-generator</a></li> 1351 * <li><a href="https://github.com/f4b6a3/uuid-creator">uuid-creator</a></li> 1352 * </ul> 1353 * 1354 * @return A UUID object representing the UUID v7. 1355 * @see "https://uuid7.com/" 1356 * @see "https://antonz.org/uuidv7/" 1357 * @see "https://mccue.dev/pages/3-11-25-life-altering-postgresql-patterns" 1358 * @see "https://www.ietf.org/archive/id/draft-peabody-dispatch-new-uuid-format-04.html#name-uuid-version-7" 1359 * @see "https://www.baeldung.com/java-generating-time-based-uuids" 1360 * @see "https://en.wikipedia.org/wiki/Universally_unique_identifier" 1361 * @see "https://buildkite.com/resources/blog/goodbye-integers-hello-uuids/" 1362 */ 1363 public static UUID computeUUIDv7() { 1364 SecureRandom secureRandom = new SecureRandom(); 1365 // Generate truly random bytes 1366 byte[] value = new byte[16]; 1367 secureRandom.nextBytes(value); 1368 // Get current timestamp in milliseconds 1369 ByteBuffer timestamp = ByteBuffer.allocate(Long.BYTES); 1370 timestamp.putLong(System.currentTimeMillis()); 1371 // Create the TIMESTAMP part of the UUID 1372 System.arraycopy(timestamp.array(), 2, value, 0, 6); 1373 // Create the VERSION and the VARIANT parts of the UUID 1374 value[6] = (byte) ((value[6] & 0x0F) | 0x70); 1375 value[8] = (byte) ((value[8] & 0x3F) | 0x80); 1376 //Create the HIGH and LOW parts of the UUID 1377 ByteBuffer buf = ByteBuffer.wrap(value); 1378 long high = buf.getLong(); 1379 long low = buf.getLong(); 1380 //Create and return the UUID object 1381 UUID uuidv7 = new UUID(high, low); 1382 return uuidv7; 1383 } 1384 1385 /** 1386 * Ensure that an XSD file does not contain any include/import/redefine instruction (prevent exposure to SSRF). 1387 * 1388 * @param xsdFilePath Filename of the XSD file to check. 1389 * @return True only if the file pass all validations. 1390 * @see "https://portswigger.net/web-security/ssrf" 1391 * @see "https://www.w3schools.com/Xml/el_import.asp" 1392 * @see "https://www.w3schools.com/xml/el_include.asp" 1393 * @see "https://www.linkedin.com/posts/righettod_appsec-appsecurity-java-activity-7344048434326188053-6Ru9" 1394 * @see "https://docs.oracle.com/en/java/javase/21/docs/api/java.xml/javax/xml/validation/SchemaFactory.html#setProperty(java.lang.String,java.lang.Object)" 1395 */ 1396 public static boolean isXSDSafe(String xsdFilePath) { 1397 boolean isSafe = false; 1398 try { 1399 File xsdFile = new File(xsdFilePath); 1400 if (xsdFile.exists() && xsdFile.canRead() && xsdFile.isFile()) { 1401 //Parse the XSD file, if an exception occur then it's imply that the XSD specified is not a valid ones 1402 //Create an schema factory throwing Exception if a external schema is specified 1403 SchemaFactory schemaFactory = SchemaFactory.newDefaultInstance(); 1404 schemaFactory.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, ""); 1405 schemaFactory.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); 1406 //Parse the schema 1407 Schema schema = schemaFactory.newSchema(xsdFile); 1408 isSafe = (schema != null); 1409 } 1410 } catch (Exception e) { 1411 isSafe = false; 1412 } 1413 return isSafe; 1414 } 1415}