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

  1. Treat the jar as hostile: do not unpack or run it; obtain it from a trusted source
  2. Inspect entry names with jar tf or unzip -l to identify the offending traversal entries
  3. Rebuild the jar with a standard tool (Maven/Gradle/jar) so entries are relative to the jar root
  4. 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

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


AI-assisted analysis of apache/hadoop@2add963021 (2026-08-22). Data as JSON: /api/errors/15d555b2c2a9e18a. Report an issue: GitHub.