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
- 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.
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
- 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
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
- Access-Check-Result
- <authResult.getErrorMessage()>
- authResult.getErrorMessage()
- authResult.getErrorMessage()
- authResult.getErrorMessage()
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)