apache/druid · error · IllegalStateException

Unzipped output path

Error message

Unzipped output path[%s] of sourceFile[%s] does not start with outDir[%s].

What it means

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.

Solutions

  1. Inspect the archive (`unzip -l`) for entries containing ../ or absolute paths; do not extract untrusted archives.
  2. Re-generate the zip with safe, relative entry names (e.g. with Apache Commons Compress or `zip` from the correct cwd).
  3. Ensure outDir's real (canonical) path matches the intended directory; avoid symlinks in outDir chains.
  4. If you truly control the archive and path, extract manually after validating each entry name yourself.

Example fix

// before (entry name escapes outDir)
// zip contains: ../../evil.sh
CompressionUtils.unzip(untrusted.zip, outDir);
// after - sanitize entries before archiving
// zip entry: evil.sh
CompressionUtils.unzip(trusted.zip, outDir.getCanonicalFile());
Defensive patterns

Strategy: try-catch

Validate before calling

// Pre-scan zip entries before extraction
try (ZipFile zf = new ZipFile(pulledFile)) {
  Enumeration<? extends ZipEntry> en = zf.entries();
  while (en.hasMoreElements()) {
    String name = en.nextElement().getName();
    if (name.contains("..") || new File(name).isAbsolute()) {
      throw new SecurityException("Unsafe zip entry: " + name);
    }
  }
}

Try / catch

try {
  CompressionUtils.unzip(pulledFile, outDir);
} catch (IllegalStateException e) {
  if (e.getMessage().startsWith("Unzipped output path")) {
    // treat archive as malicious/corrupt; quarantine and alert
    throw new SecurityException("Zip Slip attempt in " + pulledFile, e);
  }
  throw e;
}

Prevention

When it happens

Trigger: Extracting an archive whose entry names contain ../ or absolute paths that escape outDir; also triggered when outDir itself contains symlinks that canonicalize elsewhere.

Common situations: 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.

Understand the failure class

Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.

Related errors


AI-assisted analysis of apache/druid@9b90983fd2 (2026-09-07). Data as JSON: /api/errors/3065fecef926d818. Report an issue: GitHub.

Appendix: source

Thrown at processing/src/main/java/org/apache/druid/utils/CompressionUtils.java:501

                DEFAULT_RETRY_COUNT
            ).getFiles()
        );
      }
    }
    return result;
  }

  public static void validateZipOutputFile(
      String sourceFilename,
      final File outFile,
      final File outDir
  ) throws IOException
  {
    // check for evil zip exploit that allows writing output to arbitrary directories
    final File canonicalOutFile = outFile.getCanonicalFile();
    final String canonicalOutDir = outDir.getCanonicalPath();
    if (!canonicalOutFile.toPath().startsWith(canonicalOutDir)) {
      throw new ISE(
          "Unzipped output path[%s] of sourceFile[%s] does not start with outDir[%s].",
          canonicalOutFile,
          sourceFilename,
          canonicalOutDir
      );
    }
  }

  /**
   * Unzip from the input stream to the output directory, using the entry's file name as the file name in the output directory.
   * The behavior of directories in the input stream's zip is undefined.
   * If possible, it is recommended to use unzip(ByteStream, File) instead
   *
   * @param in     The input stream of the zip data. This stream is closed
   * @param outDir The directory to copy the unzipped data to
   *
   * @return The FileUtils.FileCopyResult containing information on all the files which were written
   *

View on GitHub (pinned to 9b90983fd2)