{"record":{"id":"3065fecef926d818","repo":"apache/druid","slug":"unzipped-output-path-s-of-sourcefile-s-does-no","errorCode":null,"errorMessage":"Unzipped output path[%s] of sourceFile[%s] does not start with outDir[%s].","messagePattern":"Unzipped output path\\[(.+?)\\] of sourceFile\\[(.+?)\\] does not start with outDir\\[(.+?)\\]\\.","errorType":"exception","errorClass":"IllegalStateException","httpStatus":null,"severity":"error","filePath":"processing/src/main/java/org/apache/druid/utils/CompressionUtils.java","lineNumber":501,"sourceCode":"                DEFAULT_RETRY_COUNT\n            ).getFiles()\n        );\n      }\n    }\n    return result;\n  }\n\n  public static void validateZipOutputFile(\n      String sourceFilename,\n      final File outFile,\n      final File outDir\n  ) throws IOException\n  {\n    // check for evil zip exploit that allows writing output to arbitrary directories\n    final File canonicalOutFile = outFile.getCanonicalFile();\n    final String canonicalOutDir = outDir.getCanonicalPath();\n    if (!canonicalOutFile.toPath().startsWith(canonicalOutDir)) {\n      throw new ISE(\n          \"Unzipped output path[%s] of sourceFile[%s] does not start with outDir[%s].\",\n          canonicalOutFile,\n          sourceFilename,\n          canonicalOutDir\n      );\n    }\n  }\n\n  /**\n   * Unzip from the input stream to the output directory, using the entry's file name as the file name in the output directory.\n   * The behavior of directories in the input stream's zip is undefined.\n   * If possible, it is recommended to use unzip(ByteStream, File) instead\n   *\n   * @param in     The input stream of the zip data. This stream is closed\n   * @param outDir The directory to copy the unzipped data to\n   *\n   * @return The FileUtils.FileCopyResult containing information on all the files which were written\n   *","sourceCodeStart":483,"sourceCodeEnd":519,"githubUrl":"https://github.com/apache/druid/blob/9b90983fd291f26935af934383ce360473179e4d/processing/src/main/java/org/apache/druid/utils/CompressionUtils.java#L483-L519","documentation":"Thrown by validateZipOutputFile (used by unzip and lz4DecompressDirectory) when a zip entry's resolved output path, after canonicalization, does not live inside the target directory. This is Druid's Zip Slip defense: a malicious archive containing entries like ../../../etc/passwd would otherwise write outside outDir.","triggerScenarios":"Extracting an archive whose entry names contain ../ or absolute paths that escape outDir; also triggered when outDir itself contains symlinks that canonicalize elsewhere.","commonSituations":"Pulling a corrupted or attacker-crafted zip from untrusted deep storage, hand-edited archives, or testing with zips built with ../ entry names. Also occurs when outDir is a symlink to another location and the canonical paths diverge.","solutions":["Inspect the archive (`unzip -l`) for entries containing ../ or absolute paths; do not extract untrusted archives.","Re-generate the zip with safe, relative entry names (e.g. with Apache Commons Compress or `zip` from the correct cwd).","Ensure outDir's real (canonical) path matches the intended directory; avoid symlinks in outDir chains.","If you truly control the archive and path, extract manually after validating each entry name yourself."],"exampleFix":"// before (entry name escapes outDir)\n// zip contains: ../../evil.sh\nCompressionUtils.unzip(untrusted.zip, outDir);\n// after - sanitize entries before archiving\n// zip entry: evil.sh\nCompressionUtils.unzip(trusted.zip, outDir.getCanonicalFile());","handlingStrategy":"try-catch","validationCode":"// Pre-scan zip entries before extraction\ntry (ZipFile zf = new ZipFile(pulledFile)) {\n  Enumeration<? extends ZipEntry> en = zf.entries();\n  while (en.hasMoreElements()) {\n    String name = en.nextElement().getName();\n    if (name.contains(\"..\") || new File(name).isAbsolute()) {\n      throw new SecurityException(\"Unsafe zip entry: \" + name);\n    }\n  }\n}","typeGuard":null,"tryCatchPattern":"try {\n  CompressionUtils.unzip(pulledFile, outDir);\n} catch (IllegalStateException e) {\n  if (e.getMessage().startsWith(\"Unzipped output path\")) {\n    // treat archive as malicious/corrupt; quarantine and alert\n    throw new SecurityException(\"Zip Slip attempt in \" + pulledFile, e);\n  }\n  throw e;\n}","preventionTips":["Only extract archives from trusted deep-storage sources","Pre-scan entry names for ../ and absolute paths","Avoid symlinks in the outDir chain so canonical paths match expectations","Keep this validation in place - never bypass the canonical-path check"],"tags":["security","zip","path-traversal"],"backgroundTag":"path-traversal-blocked","analyzedSha":"9b90983fd291f26935af934383ce360473179e4d","analyzedAt":"2026-09-07T13:32:30.957Z","contentChangedAt":"2026-09-07T13:32:30.957Z","schemaVersion":2},"datasetVersion":"2026-09-17T15:17:12.973Z"}