This is an automated email from the ASF dual-hosted git repository. rzo1 pushed a commit to branch tomee-10.x in repository https://gitbox.apache.org/repos/asf/tomee.git
commit 0f2c0f8dc698fa4ab69da96b70cd464ba3c80951 Author: Markus Jung <[email protected]> AuthorDate: Sun Aug 23 21:13:03 2026 +0200 validate zip extraction (cherry picked from commit b79cb05898d166e34014763de243cce292519b96) --- .../main/java/org/apache/openejb/loader/Zips.java | 9 +++ .../java/org/apache/openejb/loader/ZipsTest.java | 84 ++++++++++++++++++++++ 2 files changed, 93 insertions(+) diff --git a/container/openejb-loader/src/main/java/org/apache/openejb/loader/Zips.java b/container/openejb-loader/src/main/java/org/apache/openejb/loader/Zips.java index e1aa249402..6ec6ad10c3 100644 --- a/container/openejb-loader/src/main/java/org/apache/openejb/loader/Zips.java +++ b/container/openejb-loader/src/main/java/org/apache/openejb/loader/Zips.java @@ -49,6 +49,9 @@ public class Zips { public static void unzip(final InputStream read, final File destination, final boolean noparent) throws IOException { try { + final String destinationPath = destination.getCanonicalPath(); + final String destinationPrefix = destinationPath.endsWith(File.separator) ? destinationPath : destinationPath + File.separator; + // Open the ZIP file final ZipInputStream in = new ZipInputStream(read); @@ -61,6 +64,12 @@ public class Zips { } final File file = new File(destination, path); + // only extract entries resolving inside the destination directory + final String filePath = file.getCanonicalPath(); + if (!filePath.equals(destinationPath) && !filePath.startsWith(destinationPrefix)) { + throw new IOException("Zip entry escapes the destination directory: " + entry.getName()); + } + if (entry.isDirectory()) { Files.mkdir(file); continue; diff --git a/container/openejb-loader/src/test/java/org/apache/openejb/loader/ZipsTest.java b/container/openejb-loader/src/test/java/org/apache/openejb/loader/ZipsTest.java new file mode 100644 index 0000000000..830b1b3f4e --- /dev/null +++ b/container/openejb-loader/src/test/java/org/apache/openejb/loader/ZipsTest.java @@ -0,0 +1,84 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.openejb.loader; + +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public class ZipsTest { + + @Test + public void unzipExtractsRegularEntries() throws Exception { + final File destination = Files.mkdirs(new File("target/test/zips/normal")); + + Zips.unzip(zip("dir/file.txt", "hello"), destination, false); + + assertTrue(new File(destination, "dir/file.txt").isFile()); + } + + @Test + public void unzipRejectsEntryEscapingDestination() throws Exception { + final File destination = Files.mkdirs(new File("target/test/zips/slip/dest")); + final File victim = new File(destination.getParentFile(), "victim.txt"); + Files.delete(victim); + + try { + Zips.unzip(zip("../victim.txt", "pwned"), destination, false); + fail("expected IOException for a zip entry escaping the destination"); + } catch (final IOException expected) { + // expected + } + assertFalse("no file may be written outside the destination", victim.exists()); + } + + @Test + public void unzipNoparentStillRejectsEscapingEntry() throws Exception { + final File destination = Files.mkdirs(new File("target/test/zips/noparent/dest")); + final File victim = new File(destination.getParentFile(), "victim.txt"); + Files.delete(victim); + + try { + // after the noparent strip "x/" is removed, leaving "../victim.txt" + Zips.unzip(zip("x/../../victim.txt", "pwned"), destination, true); + fail("expected IOException for a zip entry escaping the destination"); + } catch (final IOException expected) { + // expected + } + assertFalse("no file may be written outside the destination", victim.exists()); + } + + private static InputStream zip(final String entryName, final String content) throws IOException { + final ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + try (final ZipOutputStream zip = new ZipOutputStream(bytes)) { + zip.putNextEntry(new ZipEntry(entryName)); + zip.write(content.getBytes()); + zip.closeEntry(); + } + return new ByteArrayInputStream(bytes.toByteArray()); + } +}
