apache/hadoop · critical · IOException
expanding {} would create file outside of {}
Error message
expanding {} would create file outside of {} What it means
RunJar.unjar unpacks a jar (this is the JarInputStream variant used on streams) into a target directory. For each matching entry it resolves the target file's canonical path and requires it to start with the canonical target-directory prefix; an entry such as '../../evil.sh' or any name resolving outside makes it throw IOException("expanding <entry> would create file outside of <dir>"). This is the zip-slip defense against path traversal in archive entry names.
Source
Thrown at hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/util/RunJar.java:141
* @param unpackRegex the pattern to match jar entries against
*
* @throws IOException if an I/O error has occurred or toDir
* cannot be created and does not already exist
*/
public static void unJar(InputStream inputStream, File toDir,
Pattern unpackRegex)
throws IOException {
try (JarInputStream jar = new JarInputStream(inputStream)) {
int numOfFailedLastModifiedSet = 0;
String targetDirPath = toDir.getCanonicalPath() + File.separator;
for (JarEntry entry = jar.getNextJarEntry();
entry != null;
entry = jar.getNextJarEntry()) {
if (!entry.isDirectory() &&
unpackRegex.matcher(entry.getName()).matches()) {
File file = new File(toDir, entry.getName());
if (!file.getCanonicalPath().startsWith(targetDirPath)) {
throw new IOException("expanding " + entry.getName()
+ " would create file outside of " + toDir);
}
ensureDirectory(file.getParentFile());
try (OutputStream out = Files.newOutputStream(file.toPath())) {
IOUtils.copyBytes(jar, out, BUFFER_SIZE);
}
if (!file.setLastModified(entry.getTime())) {
numOfFailedLastModifiedSet++;
}
}
}
if (numOfFailedLastModifiedSet > 0) {
LOG.warn("Could not set last modfied time for {} file(s)",
numOfFailedLastModifiedSet);
}
// ZipInputStream does not need the end of the file. Let's read it out.
// This helps with an additional TeeInputStream on the input.
IOUtils.copyBytes(inputStream, new NullOutputStream(), BUFFER_SIZE);View on GitHub (pinned to 2add963021)
Solutions
- Treat the jar as hostile: do not unpack or run it; obtain it from a trusted source
- Inspect entry names with jar tf or unzip -l to identify the offending traversal entries
- Rebuild the jar with a standard tool (Maven/Gradle/jar) so entries are relative to the jar root
- Keep a Hadoop version that includes the canonical-path zip-slip check rather than bypassing it
Example fix
# before unzip -l bad.jar # shows entries like ../../../etc/cron.d/x # after: rebuild from a clean root with relative names cd src-root && jar cf fixed.jar com/
Defensive patterns
Strategy: validation
Validate before calling
Path target = toDir.getCanonicalFile().toPath();
try (JarFile jar = new JarFile(jarFile)) {
Enumeration<JarEntry> es = jar.entries();
while (es.hasMoreElements()) {
Path resolved = target.resolve(es.nextElement().getName()).normalize();
if (!resolved.startsWith(target)) {
throw new IOException("unsafe entry (zip-slip) in " + jarFile);
}
}
} Try / catch
try { RunJar.unjar(jarFile, toDir, unpackRegex); } catch (IOException e) { if (e.getMessage() != null && e.getMessage().startsWith("expanding")) { rejectArtifact(e.getMessage()); } else throw e; } Prevention
- Only unpack jars from trusted sources
- Scan archive entry names for '../' in CI before deployment
- Build jars with standard tools so entries are relative paths
- Never catch-and-continue past this check — it is path-traversal protection
When it happens
Trigger: Unpacking a crafted or malformed jar whose entries contain '../' path segments; jars produced by broken tooling that stored absolute or traversal entry names; symlinked layouts where an entry name resolves outside the unpack root.
Common situations: Running 'hadoop jar' or calling RunJar/HadoopUnjar on an untrusted artifact; CI-built jars from nonstandard archivers; archives accepted by laxer runtimes but rejected by Hadoop's canonical-path check.
Related errors
- expanding " + entry.getName() + " would create file outside
- expanding " + entry.getName() + " would create file outside
- expanding " + entry.getName() + " would create entry outside
- Access denied: dfs.http.policy is HTTPS_ONLY.
- Wrong key length. Required ${options.getBitLength()}, but go
AI-assisted analysis of apache/hadoop@2add963021 (2026-08-22).
Data as JSON: /api/errors/15d555b2c2a9e18a.
Report an issue: GitHub.