You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@ant.apache.org by bo...@apache.org on 2022/11/04 10:32:05 UTC

[ant-ivy] branch master updated (9d7b5e13 -> 9c4802b7)

This is an automated email from the ASF dual-hosted git repository.

bodewig pushed a change to branch master
in repository https://gitbox.apache.org/repos/asf/ant-ivy.git


    from 9d7b5e13 ignore failing test
     new 03b6b8c3 CVE-2022-37865 ZipPacking allows overwriting arbitrary files
     new 3f374602 CVE-2022-37866 prevent path-traversal with bogus module coordinates
     new 9c4802b7 update release notes with CVE information

The 3 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 asciidoc/release-notes.adoc                        |   5 +-
 src/java/org/apache/ivy/core/IvyPatternHelper.java |  72 +++++++++++---
 .../core/cache/DefaultRepositoryCacheManager.java  |  25 ++++-
 .../core/cache/DefaultResolutionCacheManager.java  |  12 +++
 src/java/org/apache/ivy/core/pack/ZipPacking.java  |  11 ++-
 .../org/apache/ivy/core/resolve/ResolveEngine.java |   4 +
 .../apache/ivy/core/retrieve/RetrieveEngine.java   |  15 ++-
 .../ivy/plugins/report/XmlReportOutputter.java     |   4 +
 .../plugins/repository/file/FileRepository.java    |  12 ++-
 src/java/org/apache/ivy/util/FileUtil.java         |  62 +++++++++++++
 test/java/org/apache/ivy/ant/FileUtilTest.java     |  72 ++++++++++++++
 .../cache/DefaultRepositoryCacheManagerTest.java   |  58 ++++++++++++
 .../cache/DefaultResolutionCacheManagerTest.java   |  64 +++++++++++++
 .../org/apache/ivy/core/pack/ZipPackingTest.java   |  72 ++++++++++++++
 .../apache/ivy/core/resolve/ResolveEngineTest.java |  35 +++++++
 .../org/apache/ivy/core/retrieve/RetrieveTest.java | 103 +++++++++++++++++++++
 .../repository/file/FileRepositoryTest.java        |  85 +++++++++++++++++
 .../org/apache/ivy/util/IvyPatternHelperTest.java  |  91 ++++++++++++++++++
 test/zip/test.zip                                  | Bin 0 -> 554 bytes
 19 files changed, 778 insertions(+), 24 deletions(-)
 create mode 100644 test/java/org/apache/ivy/core/cache/DefaultResolutionCacheManagerTest.java
 create mode 100644 test/java/org/apache/ivy/core/pack/ZipPackingTest.java
 create mode 100644 test/java/org/apache/ivy/plugins/repository/file/FileRepositoryTest.java
 create mode 100644 test/zip/test.zip


[ant-ivy] 03/03: update release notes with CVE information

Posted by bo...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

bodewig pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ant-ivy.git

commit 9c4802b70c430019c083e39a8200f239bb7f8929
Author: Stefan Bodewig <bo...@apache.org>
AuthorDate: Tue Nov 1 12:31:33 2022 +0100

    update release notes with CVE information
---
 asciidoc/release-notes.adoc | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/asciidoc/release-notes.adoc b/asciidoc/release-notes.adoc
index c59c5135..08fc2b79 100644
--- a/asciidoc/release-notes.adoc
+++ b/asciidoc/release-notes.adoc
@@ -19,7 +19,7 @@
 
 = Ivy Release Announcement
 
-XXXX Date XXXX - The Apache Ivy project is pleased to announce its 2.5.1 release.
+4th November 2022 - The Apache Ivy project is pleased to announce its 2.5.1 release.
 
 == What is Ivy?
 Apache Ivy is a tool for managing (recording, tracking, resolving and reporting) project dependencies, characterized by flexibility,
@@ -37,6 +37,7 @@ More information about the project can be found on the website link:https://ant.
 Key features of this 2.5.1 release are:
 
     * Ivy now requires a minimum of Java 8 runtime.
+    * Fixes two Security Vulnerabilities, see link:https://ant.apache.org/ivy/security.html[the scurity page] for details.
 
 
 == List of Changes in this Release
@@ -53,6 +54,8 @@ For details about the following changes, check our JIRA install at link:https://
 - FIX: ivy:retrieve Ant task relied on the default HTTP header "Accept" which caused problems with servers that interpret it strictly (e.g. AWS CodeArtifact) (jira:IVY-1632[])
 
 - IMPROVEMENT: Ivy command now accepts a URL for the -settings option (jira:IVY-1615[])
+- FIX: CVE-2022-37865 allow create/overwrite any file on the system (see link:https://ant.apache.org/ivy/security.html[])
+- FIX: CVE-2022-37866 Path traversal in patterns (see link:https://ant.apache.org/ivy/security.html[])
 
 
 ////


[ant-ivy] 02/03: CVE-2022-37866 prevent path-traversal with bogus module coordinates

Posted by bo...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

bodewig pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ant-ivy.git

commit 3f374602d4d63691398951b9af692960d019f4d9
Author: Stefan Bodewig <bo...@apache.org>
AuthorDate: Sun Aug 21 18:54:43 2022 +0200

    CVE-2022-37866 prevent path-traversal with bogus module coordinates
---
 src/java/org/apache/ivy/core/IvyPatternHelper.java |  72 +++++++++++---
 .../core/cache/DefaultRepositoryCacheManager.java  |  25 ++++-
 .../core/cache/DefaultResolutionCacheManager.java  |  12 +++
 .../org/apache/ivy/core/resolve/ResolveEngine.java |   4 +
 .../apache/ivy/core/retrieve/RetrieveEngine.java   |  15 ++-
 .../ivy/plugins/report/XmlReportOutputter.java     |   4 +
 .../plugins/repository/file/FileRepository.java    |  12 ++-
 .../cache/DefaultRepositoryCacheManagerTest.java   |  58 ++++++++++++
 .../cache/DefaultResolutionCacheManagerTest.java   |  64 +++++++++++++
 .../apache/ivy/core/resolve/ResolveEngineTest.java |  35 +++++++
 .../org/apache/ivy/core/retrieve/RetrieveTest.java | 103 +++++++++++++++++++++
 .../repository/file/FileRepositoryTest.java        |  85 +++++++++++++++++
 .../org/apache/ivy/util/IvyPatternHelperTest.java  |  91 ++++++++++++++++++
 13 files changed, 559 insertions(+), 21 deletions(-)

diff --git a/src/java/org/apache/ivy/core/IvyPatternHelper.java b/src/java/org/apache/ivy/core/IvyPatternHelper.java
index 3dacb7d5..3614ac78 100644
--- a/src/java/org/apache/ivy/core/IvyPatternHelper.java
+++ b/src/java/org/apache/ivy/core/IvyPatternHelper.java
@@ -22,6 +22,7 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Stack;
+import java.util.StringTokenizer;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
@@ -135,7 +136,7 @@ public final class IvyPatternHelper {
                 if (token.indexOf(':') > 0) {
                     token = token.substring(token.indexOf(':') + 1);
                 }
-                tokens.put(token, entry.getValue());
+                tokens.put(token, new Validated(token, entry.getValue()));
             }
         }
         if (extraArtifactAttributes != null) {
@@ -144,19 +145,19 @@ public final class IvyPatternHelper {
                 if (token.indexOf(':') > 0) {
                     token = token.substring(token.indexOf(':') + 1);
                 }
-                tokens.put(token, entry.getValue());
+                tokens.put(token, new Validated(token, entry.getValue()));
             }
         }
-        tokens.put(ORGANISATION_KEY, org == null ? "" : org);
-        tokens.put(ORGANISATION_KEY2, org == null ? "" : org);
+        tokens.put(ORGANISATION_KEY, org == null ? "" : new Validated(ORGANISATION_KEY, org));
+        tokens.put(ORGANISATION_KEY2, org == null ? "" : new Validated(ORGANISATION_KEY2, org));
         tokens.put(ORGANISATION_PATH_KEY, org == null ? "" : org.replace('.', '/'));
-        tokens.put(MODULE_KEY, module == null ? "" : module);
-        tokens.put(BRANCH_KEY, branch == null ? "" : branch);
-        tokens.put(REVISION_KEY, revision == null ? "" : revision);
-        tokens.put(ARTIFACT_KEY, artifact == null ? module : artifact);
-        tokens.put(TYPE_KEY, type == null ? "jar" : type);
-        tokens.put(EXT_KEY, ext == null ? "jar" : ext);
-        tokens.put(CONF_KEY, conf == null ? "default" : conf);
+        tokens.put(MODULE_KEY, module == null ? "" : new Validated(MODULE_KEY, module));
+        tokens.put(BRANCH_KEY, branch == null ? "" : new Validated(BRANCH_KEY, branch));
+        tokens.put(REVISION_KEY, revision == null ? "" : new Validated(REVISION_KEY, revision));
+        tokens.put(ARTIFACT_KEY, new Validated(ARTIFACT_KEY, artifact == null ? module : artifact));
+        tokens.put(TYPE_KEY, type == null ? "jar" : new Validated(TYPE_KEY, type));
+        tokens.put(EXT_KEY, ext == null ? "jar" : new Validated(EXT_KEY, ext));
+        tokens.put(CONF_KEY, conf == null ? "default" : new Validated(CONF_KEY, conf));
         if (origin == null) {
             tokens.put(ORIGINAL_ARTIFACTNAME_KEY, new OriginalArtifactNameValue(org, module,
                     branch, revision, artifact, type, ext, extraModuleAttributes,
@@ -328,7 +329,9 @@ public final class IvyPatternHelper {
                     + pattern);
         }
 
-        return buffer.toString();
+        String afterTokenSubstitution = buffer.toString();
+        checkAgainstPathTraversal(pattern, afterTokenSubstitution);
+        return afterTokenSubstitution;
     }
 
     public static String substituteVariable(String pattern, String variable, String value) {
@@ -518,4 +521,49 @@ public final class IvyPatternHelper {
         }
         return pattern.substring(startIndex + 1, endIndex);
     }
+
+    /**
+     * This class returns a captured value after validating it doesn't
+     * contain any path traversal sequence.
+     *
+     * <p>{@code toString}</p> will be invoked when the value is
+     * actually used as a token inside of a pattern passed to {@link
+     * #substituteTokens}.</p>
+     */
+    private static class Validated {
+        private final String tokenName, tokenValue;
+
+        private Validated(String tokenName, String tokenValue) {
+            this.tokenName = tokenName;
+            this.tokenValue = tokenValue;
+        }
+
+        @Override
+        public String toString() {
+            if (tokenValue != null && !tokenValue.isEmpty()) {
+                StringTokenizer tok = new StringTokenizer(tokenValue.replace("\\", "/"), "/");
+                while (tok.hasMoreTokens()) {
+                    if ("..".equals(tok.nextToken())) {
+                        throw new IllegalArgumentException("\'" + tokenName + "\' value " + tokenValue + " contains an illegal path sequence");
+                    }
+                }
+            }
+            return tokenValue;
+        }
+    }
+
+    private static void checkAgainstPathTraversal(String pattern, String afterTokenSubstitution) {
+        String root = getTokenRoot(pattern);
+        int rootLen = root.length(); // it is OK to have a token root containing .. sequences
+        if (root.endsWith("/") || root.endsWith("\\")) {
+            --rootLen;
+        }
+        String patternedPartWithNormalizedSlashes =
+            afterTokenSubstitution.substring(rootLen).replace("\\", "/");
+        if (patternedPartWithNormalizedSlashes.endsWith("/..")
+            || patternedPartWithNormalizedSlashes.indexOf("/../") >= 0) {
+            throw new IllegalArgumentException("path after token expansion contains an illegal sequence");
+        }
+    }
+
 }
diff --git a/src/java/org/apache/ivy/core/cache/DefaultRepositoryCacheManager.java b/src/java/org/apache/ivy/core/cache/DefaultRepositoryCacheManager.java
index 497a5363..234810a6 100644
--- a/src/java/org/apache/ivy/core/cache/DefaultRepositoryCacheManager.java
+++ b/src/java/org/apache/ivy/core/cache/DefaultRepositoryCacheManager.java
@@ -683,8 +683,10 @@ public class DefaultRepositoryCacheManager implements RepositoryCacheManager, Iv
     }
 
     private PropertiesFile getCachedDataFile(ModuleRevisionId mRevId) {
-        return new PropertiesFile(new File(getRepositoryCacheRoot(), IvyPatternHelper.substitute(
-            getDataFilePattern(), mRevId)), "ivy cached data file for " + mRevId);
+        File file = new File(getRepositoryCacheRoot(), IvyPatternHelper.substitute(
+            getDataFilePattern(), mRevId));
+        assertInsideCache(file);
+        return new PropertiesFile(file, "ivy cached data file for " + mRevId);
     }
 
     /**
@@ -693,9 +695,10 @@ public class DefaultRepositoryCacheManager implements RepositoryCacheManager, Iv
      */
     private PropertiesFile getCachedDataFile(String resolverName, ModuleRevisionId mRevId) {
         // we append ".${resolverName} onto the end of the regular ivydata location
-        return new PropertiesFile(new File(getRepositoryCacheRoot(),
-                IvyPatternHelper.substitute(getDataFilePattern(), mRevId) + "." + resolverName),
-                "ivy cached data file for " + mRevId);
+        File file = new File(getRepositoryCacheRoot(),
+            IvyPatternHelper.substitute(getDataFilePattern(), mRevId) + "." + resolverName);
+        assertInsideCache(file);
+        return new PropertiesFile(file, "ivy cached data file for " + mRevId);
     }
 
     public ResolvedModuleRevision findModuleInCache(DependencyDescriptor dd,
@@ -1029,6 +1032,7 @@ public class DefaultRepositoryCacheManager implements RepositoryCacheManager, Iv
                                         + resourceResolver
                                         + "': pointing repository to ivy cache is forbidden !");
                             }
+                            assertInsideCache(archiveFile);
                             if (listener != null) {
                                 listener.startArtifactDownload(this, artifactRef, artifact, origin);
                             }
@@ -1147,6 +1151,7 @@ public class DefaultRepositoryCacheManager implements RepositoryCacheManager, Iv
                         }
 
                         // actual download
+                        assertInsideCache(archiveFile);
                         if (archiveFile.exists()) {
                             archiveFile.delete();
                         }
@@ -1534,6 +1539,16 @@ public class DefaultRepositoryCacheManager implements RepositoryCacheManager, Iv
         Message.debug("\t\tchangingMatcher: " + getChangingMatcherName());
     }
 
+    /**
+     * @throws IllegalArgumentException if the given path points outside of the cache.
+     */
+    public final void assertInsideCache(File fileInCache) {
+        File root = getRepositoryCacheRoot();
+        if (root != null && !FileUtil.isLeadingPath(root, fileInCache)) {
+            throw new IllegalArgumentException(fileInCache + " is outside of the cache");
+        }
+    }
+
     /**
      * If the {@link ArtifactOrigin#getLocation() location of the artifact origin} is a
      * {@code file:} scheme URI, then this method parses that URI and returns back the
diff --git a/src/java/org/apache/ivy/core/cache/DefaultResolutionCacheManager.java b/src/java/org/apache/ivy/core/cache/DefaultResolutionCacheManager.java
index 52c3400f..e901f1fb 100644
--- a/src/java/org/apache/ivy/core/cache/DefaultResolutionCacheManager.java
+++ b/src/java/org/apache/ivy/core/cache/DefaultResolutionCacheManager.java
@@ -180,6 +180,7 @@ public class DefaultResolutionCacheManager implements ResolutionCacheManager, Iv
             IOException {
         ModuleRevisionId mrevId = md.getResolvedModuleRevisionId();
         File ivyFileInCache = getResolvedIvyFileInCache(mrevId);
+        assertInsideCache(ivyFileInCache);
         md.toIvyFile(ivyFileInCache);
 
         Properties paths = new Properties();
@@ -188,12 +189,22 @@ public class DefaultResolutionCacheManager implements ResolutionCacheManager, Iv
         if (!paths.isEmpty()) {
             File parentsFile = getResolvedIvyPropertiesInCache(ModuleRevisionId.newInstance(mrevId,
                 mrevId.getRevision() + "-parents"));
+            assertInsideCache(parentsFile);
             FileOutputStream out = new FileOutputStream(parentsFile);
             paths.store(out, null);
             out.close();
         }
     }
 
+    /**
+     * @throws IllegalArgumentException if the given path points outside of the cache.
+     */
+    public final void assertInsideCache(File fileInCache) {
+        if (!FileUtil.isLeadingPath(getResolutionCacheRoot(), fileInCache)) {
+            throw new IllegalArgumentException(fileInCache + " is outside of the cache");
+        }
+    }
+
     private void saveLocalParents(ModuleRevisionId baseMrevId, ModuleDescriptor md, File mdFile,
             Properties paths) throws ParseException, IOException {
         for (ExtendsDescriptor parent : md.getInheritedDescriptors()) {
@@ -206,6 +217,7 @@ public class DefaultResolutionCacheManager implements ResolutionCacheManager, Iv
             ModuleRevisionId pRevId = ModuleRevisionId.newInstance(baseMrevId,
                 baseMrevId.getRevision() + "-parent." + paths.size());
             File parentFile = getResolvedIvyFileInCache(pRevId);
+            assertInsideCache(parentFile);
             parentMd.toIvyFile(parentFile);
 
             paths.setProperty(mdFile.getName() + "|" + parent.getLocation(),
diff --git a/src/java/org/apache/ivy/core/resolve/ResolveEngine.java b/src/java/org/apache/ivy/core/resolve/ResolveEngine.java
index 7333e32e..d746bb07 100644
--- a/src/java/org/apache/ivy/core/resolve/ResolveEngine.java
+++ b/src/java/org/apache/ivy/core/resolve/ResolveEngine.java
@@ -39,6 +39,7 @@ import org.apache.ivy.Ivy;
 import org.apache.ivy.core.IvyContext;
 import org.apache.ivy.core.LogOptions;
 import org.apache.ivy.core.cache.ArtifactOrigin;
+import org.apache.ivy.core.cache.DefaultResolutionCacheManager;
 import org.apache.ivy.core.cache.ResolutionCacheManager;
 import org.apache.ivy.core.event.EventManager;
 import org.apache.ivy.core.event.download.PrepareDownloadEvent;
@@ -262,6 +263,9 @@ public class ResolveEngine {
             // this is used by the deliver task to resolve dynamic revisions to static ones
             File ivyPropertiesInCache = cacheManager.getResolvedIvyPropertiesInCache(md
                     .getResolvedModuleRevisionId());
+            if (cacheManager instanceof DefaultResolutionCacheManager) {
+                ((DefaultResolutionCacheManager) cacheManager).assertInsideCache(ivyPropertiesInCache);
+            }
             Properties props = new Properties();
             if (dependencies.length > 0) {
                 Map<ModuleId, ModuleRevisionId> forcedRevisions = new HashMap<>();
diff --git a/src/java/org/apache/ivy/core/retrieve/RetrieveEngine.java b/src/java/org/apache/ivy/core/retrieve/RetrieveEngine.java
index d50f047c..b6709ff6 100644
--- a/src/java/org/apache/ivy/core/retrieve/RetrieveEngine.java
+++ b/src/java/org/apache/ivy/core/retrieve/RetrieveEngine.java
@@ -290,6 +290,11 @@ public class RetrieveEngine {
         String destIvyPattern = IvyPatternHelper.substituteVariables(options.getDestIvyPattern(),
             settings.getVariables());
 
+        File fileRetrieveRoot = settings.resolveFile(IvyPatternHelper
+                .getTokenRoot(destFilePattern));
+        File ivyRetrieveRoot = destIvyPattern == null ? null : settings
+                .resolveFile(IvyPatternHelper.getTokenRoot(destIvyPattern));
+
         // find what we must retrieve where
 
         // ArtifactDownloadReport source -> Set (String copyDestAbsolutePath)
@@ -340,6 +345,7 @@ public class RetrieveEngine {
                 }
 
                 String destPattern = "ivy".equals(adr.getType()) ? destIvyPattern : destFilePattern;
+                File root = "ivy".equals(adr.getType()) ? ivyRetrieveRoot : fileRetrieveRoot;
 
                 if (!"ivy".equals(adr.getType())
                         && !options.getArtifactFilter().accept(adr.getArtifact())) {
@@ -357,7 +363,14 @@ public class RetrieveEngine {
                     dest = new HashSet<>();
                     artifactsToCopy.put(adr, dest);
                 }
-                String copyDest = settings.resolveFile(destFileName).getAbsolutePath();
+                File copyDestFile = settings.resolveFile(destFileName).getAbsoluteFile();
+                if (root != null &&
+                    !FileUtil.isLeadingPath(root, copyDestFile)) {
+                    Message.warn("not retrieving artifact " + artifact + " as its destination "
+                                 + copyDestFile + " is not inside " + root);
+                    continue;
+                }
+                String copyDest = copyDestFile.getPath();
 
                 String[] destinations = new String[] {copyDest};
                 if (options.getMapper() != null) {
diff --git a/src/java/org/apache/ivy/plugins/report/XmlReportOutputter.java b/src/java/org/apache/ivy/plugins/report/XmlReportOutputter.java
index c4a31f3d..76251029 100644
--- a/src/java/org/apache/ivy/plugins/report/XmlReportOutputter.java
+++ b/src/java/org/apache/ivy/plugins/report/XmlReportOutputter.java
@@ -22,6 +22,7 @@ import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.OutputStream;
 
+import org.apache.ivy.core.cache.DefaultResolutionCacheManager;
 import org.apache.ivy.core.cache.ResolutionCacheManager;
 import org.apache.ivy.core.report.ConfigurationResolveReport;
 import org.apache.ivy.core.report.ResolveReport;
@@ -52,6 +53,9 @@ public class XmlReportOutputter implements ReportOutputter {
             ResolutionCacheManager cacheMgr) throws IOException {
         File reportFile = cacheMgr.getConfigurationResolveReportInCache(resolveId,
             report.getConfiguration());
+        if (cacheMgr instanceof DefaultResolutionCacheManager) {
+            ((DefaultResolutionCacheManager) cacheMgr).assertInsideCache(reportFile);
+        }
         File reportParentDir = reportFile.getParentFile();
         reportParentDir.mkdirs();
         OutputStream stream = new FileOutputStream(reportFile);
diff --git a/src/java/org/apache/ivy/plugins/repository/file/FileRepository.java b/src/java/org/apache/ivy/plugins/repository/file/FileRepository.java
index fa13de76..5de1bb31 100644
--- a/src/java/org/apache/ivy/plugins/repository/file/FileRepository.java
+++ b/src/java/org/apache/ivy/plugins/repository/file/FileRepository.java
@@ -49,13 +49,15 @@ public class FileRepository extends AbstractRepository {
     }
 
     public void get(String source, File destination) throws IOException {
+        File s = getFile(source);
         fireTransferInitiated(getResource(source), TransferEvent.REQUEST_GET);
-        copy(getFile(source), destination, true);
+        copy(s, destination, true);
     }
 
     public void put(File source, String destination, boolean overwrite) throws IOException {
+        File d = getFile(destination);
         fireTransferInitiated(getResource(destination), TransferEvent.REQUEST_PUT);
-        copy(source, getFile(destination), overwrite);
+        copy(source, d, overwrite);
     }
 
     public void move(File src, File dest) throws IOException {
@@ -112,7 +114,11 @@ public class FileRepository extends AbstractRepository {
         if (baseDir == null) {
             return Checks.checkAbsolute(source, "source");
         }
-        return FileUtil.resolveFile(baseDir, source);
+        File file = FileUtil.resolveFile(baseDir, source);
+        if (!FileUtil.isLeadingPath(baseDir, file)) {
+            throw new IllegalArgumentException(source + " outside of repository root");
+        }
+        return file;
     }
 
     public boolean isLocal() {
diff --git a/test/java/org/apache/ivy/core/cache/DefaultRepositoryCacheManagerTest.java b/test/java/org/apache/ivy/core/cache/DefaultRepositoryCacheManagerTest.java
index 48ccd1e3..b88886d6 100644
--- a/test/java/org/apache/ivy/core/cache/DefaultRepositoryCacheManagerTest.java
+++ b/test/java/org/apache/ivy/core/cache/DefaultRepositoryCacheManagerTest.java
@@ -19,11 +19,13 @@ package org.apache.ivy.core.cache;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 import java.io.File;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.PrintWriter;
+import java.net.URL;
 import java.util.Date;
 
 import org.apache.ivy.Ivy;
@@ -35,14 +37,19 @@ import org.apache.ivy.core.module.descriptor.DependencyDescriptor;
 import org.apache.ivy.core.module.descriptor.ModuleDescriptor;
 import org.apache.ivy.core.module.id.ModuleId;
 import org.apache.ivy.core.module.id.ModuleRevisionId;
+import org.apache.ivy.core.report.ArtifactDownloadReport;
+import org.apache.ivy.core.report.DownloadStatus;
 import org.apache.ivy.core.resolve.ResolvedModuleRevision;
 import org.apache.ivy.core.settings.IvySettings;
 import org.apache.ivy.plugins.parser.xml.XmlModuleDescriptorWriter;
+import org.apache.ivy.plugins.repository.ArtifactResourceResolver;
 import org.apache.ivy.plugins.repository.BasicResource;
 import org.apache.ivy.plugins.repository.Resource;
 import org.apache.ivy.plugins.repository.ResourceDownloader;
+import org.apache.ivy.plugins.repository.url.URLResource;
 import org.apache.ivy.plugins.resolver.MockResolver;
 import org.apache.ivy.plugins.resolver.util.ResolvedResource;
+import org.apache.ivy.plugins.resolver.util.ResolvedResource;
 import org.apache.ivy.util.DefaultMessageLogger;
 import org.apache.ivy.util.Message;
 import org.apache.tools.ant.Project;
@@ -138,6 +145,57 @@ public class DefaultRepositoryCacheManagerTest {
         assertTrue(ArtifactOrigin.isUnknown(found));
     }
 
+    @Test
+    public void wontWritePropertiesOutsideOfCache() {
+        cacheManager.setDataFilePattern("a/../../../../../../");
+        try {
+            cacheManager.saveArtifactOrigin(artifact, origin);
+            fail("expected an exception");
+        } catch (IllegalArgumentException ex) {
+            // expected
+        }
+
+        ModuleId mi = new ModuleId("org", "module");
+        ModuleRevisionId mridLatest = new ModuleRevisionId(mi, "trunk", "latest.integration");
+        try {
+            cacheManager.saveResolvedRevision("resolver1", mridLatest, "1.1");
+            fail("expected an exception");
+        } catch (IllegalArgumentException ex) {
+            // expected
+        }
+    }
+
+    @Test
+    public void wontDownloadOutsideOfCache() throws Exception {
+        DefaultRepositoryCacheManager mgr = new DefaultRepositoryCacheManager() {
+            {
+                setUseOrigin(false);
+                setSettings(ivy.getSettings());
+                setBasedir(cacheManager.getBasedir());
+            }
+
+            @Override
+            public String getArchivePathInCache(Artifact artifact, ArtifactOrigin origin) {
+                return "../foo.txt";
+            }
+        };
+
+        ArtifactResourceResolver resolver = new ArtifactResourceResolver() {
+            @Override
+            public ResolvedResource resolve(Artifact artifact) {
+                try {
+                    return new ResolvedResource(new URLResource(new URL("https://ant.apache.org/")), "latest");
+                } catch (Exception ex) {
+                    throw new RuntimeException(ex);
+                }
+            }
+        };
+
+        ArtifactDownloadReport report = mgr.download(artifact, resolver, null, new CacheDownloadOptions());
+        assertEquals(DownloadStatus.FAILED, report.getDownloadStatus());
+        assertTrue(report.getDownloadDetails().contains("is outside"));
+    }
+
     @Test
     @Ignore
     public void testLatestIntegrationIsCachedPerResolver() throws Exception {
diff --git a/test/java/org/apache/ivy/core/cache/DefaultResolutionCacheManagerTest.java b/test/java/org/apache/ivy/core/cache/DefaultResolutionCacheManagerTest.java
new file mode 100644
index 00000000..45a9c7d9
--- /dev/null
+++ b/test/java/org/apache/ivy/core/cache/DefaultResolutionCacheManagerTest.java
@@ -0,0 +1,64 @@
+/*
+ *  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
+ *
+ *      https://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.ivy.core.cache;
+
+import static org.junit.Assert.fail;
+
+import org.apache.ivy.core.module.descriptor.DefaultModuleDescriptor;
+import org.apache.ivy.core.module.id.ModuleRevisionId;
+import org.apache.ivy.util.FileUtil;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.File;
+
+public class DefaultResolutionCacheManagerTest {
+
+    private File cacheDir;
+
+    @Before
+    public void setUp() throws Exception {
+        cacheDir = new File("build/cache");
+        cacheDir.mkdirs();
+    }
+
+    @After
+    public void tearDown() {
+        if (cacheDir != null && cacheDir.exists()) {
+            FileUtil.forceDelete(cacheDir);
+        }
+    }
+
+    @Test
+    public void wontWriteIvyFileOutsideOfCache() throws Exception {
+        DefaultResolutionCacheManager cm = new DefaultResolutionCacheManager(cacheDir) {
+            @Override
+            public File getResolvedIvyFileInCache(ModuleRevisionId mrid) {
+                return new File(getResolutionCacheRoot(), "../test.ivy.xml");
+            }
+        };
+        ModuleRevisionId mrid = ModuleRevisionId.newInstance("org", "name", "rev");
+        try {
+            cm.saveResolvedModuleDescriptor(DefaultModuleDescriptor.newDefaultInstance(mrid));
+            fail("expected exception");
+        } catch (IllegalArgumentException ex) {
+            // expected
+        }
+    }
+}
diff --git a/test/java/org/apache/ivy/core/resolve/ResolveEngineTest.java b/test/java/org/apache/ivy/core/resolve/ResolveEngineTest.java
index 243e4112..d4251ed2 100644
--- a/test/java/org/apache/ivy/core/resolve/ResolveEngineTest.java
+++ b/test/java/org/apache/ivy/core/resolve/ResolveEngineTest.java
@@ -22,6 +22,7 @@ import java.util.Date;
 
 import org.apache.ivy.Ivy;
 import org.apache.ivy.core.cache.ArtifactOrigin;
+import org.apache.ivy.core.cache.DefaultResolutionCacheManager;
 import org.apache.ivy.core.module.descriptor.Artifact;
 import org.apache.ivy.core.module.descriptor.DefaultArtifact;
 import org.apache.ivy.core.module.id.ModuleRevisionId;
@@ -39,6 +40,7 @@ import org.junit.Test;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 public class ResolveEngineTest {
 
@@ -89,6 +91,39 @@ public class ResolveEngineTest {
                     "jar", "jar"), new File("test/repositories/1/org1/mod1.1/jars/mod1.1-1.0.jar"));
     }
 
+    @Test
+    public void wontWriteResolvedDependenciesOutsideOfCache() throws Exception {
+        DefaultResolutionCacheManager orig = (DefaultResolutionCacheManager) ivy.getSettings()
+            .getResolutionCacheManager();
+
+        DefaultResolutionCacheManager fake = new DefaultResolutionCacheManager() {
+            {
+                setBasedir(orig.getBasedir());
+                setSettings(ivy.getSettings());
+            }
+
+            @Override
+            public File getResolvedIvyPropertiesInCache(ModuleRevisionId mrid) {
+                return new File(getBasedir(), "../foo.properties");
+            }
+        };
+
+        ivy.getSettings().setResolutionCacheManager(fake);
+        ResolveEngine engine = new ResolveEngine(ivy.getSettings(), ivy.getEventManager(),
+                ivy.getSortEngine());
+
+        ResolveOptions options = new ResolveOptions();
+        options.setConfs(new String[] {"*"});
+
+        ModuleRevisionId mRevId = ModuleRevisionId.parse("org1#mod1.1;1.0");
+        try {
+            engine.resolve(mRevId, options, true);
+            fail("expected an exception");
+        } catch (IllegalArgumentException ex) {
+            // expected
+        }
+    }
+
     /**
      * Tests that setting the dictator resolver on the resolve engine doesn't change the
      * dependency resolver set in the Ivy settings. See IVY-1618 for details.
diff --git a/test/java/org/apache/ivy/core/retrieve/RetrieveTest.java b/test/java/org/apache/ivy/core/retrieve/RetrieveTest.java
index e3157f1b..742202a3 100644
--- a/test/java/org/apache/ivy/core/retrieve/RetrieveTest.java
+++ b/test/java/org/apache/ivy/core/retrieve/RetrieveTest.java
@@ -20,12 +20,15 @@ package org.apache.ivy.core.retrieve;
 import org.apache.ivy.Ivy;
 import org.apache.ivy.TestHelper;
 import org.apache.ivy.core.IvyPatternHelper;
+import org.apache.ivy.core.cache.ResolutionCacheManager;
 import org.apache.ivy.core.event.IvyEvent;
 import org.apache.ivy.core.event.IvyListener;
 import org.apache.ivy.core.event.retrieve.EndRetrieveArtifactEvent;
 import org.apache.ivy.core.event.retrieve.EndRetrieveEvent;
 import org.apache.ivy.core.event.retrieve.StartRetrieveArtifactEvent;
 import org.apache.ivy.core.event.retrieve.StartRetrieveEvent;
+import org.apache.ivy.core.module.descriptor.Configuration;
+import org.apache.ivy.core.module.descriptor.DefaultModuleDescriptor;
 import org.apache.ivy.core.module.descriptor.ModuleDescriptor;
 import org.apache.ivy.core.module.id.ModuleId;
 import org.apache.ivy.core.module.id.ModuleRevisionId;
@@ -36,8 +39,11 @@ import org.apache.ivy.util.DefaultMessageLogger;
 import org.apache.ivy.util.Message;
 import org.apache.ivy.util.MockMessageLogger;
 import org.apache.tools.ant.Project;
+import org.apache.tools.ant.taskdefs.Copy;
 import org.apache.tools.ant.taskdefs.Delete;
 import org.apache.tools.ant.taskdefs.condition.JavaVersion;
+import org.apache.tools.ant.types.FilterChain;
+import org.apache.tools.ant.filters.TokenFilter.ReplaceString;
 import org.junit.After;
 import org.junit.Assume;
 import org.junit.Before;
@@ -51,6 +57,7 @@ import java.nio.file.Files;
 import java.nio.file.LinkOption;
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.text.ParseException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -61,6 +68,7 @@ import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 public class RetrieveTest {
 
@@ -135,6 +143,47 @@ public class RetrieveTest {
             "jar", "jar", "default")).exists());
     }
 
+    @Test
+    public void wontRetrieveOutsideOfDestRoot() throws Exception {
+        ResolveReport report = ivy.resolve(new File(
+                "test/repositories/1/org1/mod1.1/ivys/ivy-1.0.xml").toURI().toURL(),
+            getResolveOptions(new String[] {"*"}));
+        assertNotNull(report);
+        ModuleDescriptor md = report.getModuleDescriptor();
+        assertNotNull(md);
+
+        Ivy testIvy = Ivy.newInstance();
+        testIvy.configure(new File("test/repositories/ivysettings.xml"));
+        testIvy.getSettings()
+            .setResolutionCacheManager(new FindAllResolutionCacheManager(ivy.getResolutionCacheManager(), md));
+
+        Copy copy = new Copy();
+        copy.setProject(new Project());
+        copy.setFile(new File("build/cache/org1-mod1.1-default.xml"));
+        copy.setTofile(new File("build/cache/fake-default.xml"));
+        FilterChain fc = copy.createFilterChain();
+        ReplaceString rsOrg = new ReplaceString();
+        rsOrg.setFrom("organisation=\"org1\"");
+        rsOrg.setTo("organisation=\"fake\"");
+        fc.addReplaceString(rsOrg);
+        copy.setOverwrite(true);
+        copy.execute();
+
+        MockMessageLogger mockLogger = new MockMessageLogger();
+        Message.setDefaultLogger(mockLogger);
+
+        ModuleRevisionId id = ModuleRevisionId.newInstance("fake", "a", "1.1");
+        ModuleDescriptor fake = DefaultModuleDescriptor.newDefaultInstance(id);
+        String pattern = "build/[organisation]/../../../[artifact]-[revision].[ext]";
+        try {
+            testIvy.retrieve(fake.getModuleRevisionId(),
+                             getRetrieveOptions().setDestArtifactPattern(pattern));
+            fail("expected an exception");
+        } catch (RuntimeException ex) {
+            assertTrue(ex.getCause() instanceof IllegalArgumentException);
+        }
+    }
+
     @Test
     public void testRetrieveSameFileConflict() throws Exception {
         // mod1.1 depends on mod1.2
@@ -610,4 +659,58 @@ public class RetrieveTest {
         return new ResolveOptions().setConfs(confs);
     }
 
+
+    private static class FindAllResolutionCacheManager implements ResolutionCacheManager {
+
+        private final ResolutionCacheManager real;
+        private final ModuleRevisionId staticMrid;
+        private final ModuleDescriptor staticModuleDescriptor;
+        private static final String RESOLVE_ID = "org1-mod1.1";
+
+        private FindAllResolutionCacheManager(ResolutionCacheManager real, ModuleDescriptor md) {
+            this.real = real;
+            staticModuleDescriptor = md;
+            staticMrid = md.getModuleRevisionId();
+        }
+
+        public File getResolutionCacheRoot() {
+            return real.getResolutionCacheRoot();
+        }
+
+        public File getResolvedIvyFileInCache(ModuleRevisionId mrid) {
+            return real.getResolvedIvyFileInCache(staticMrid);
+        }
+
+        public File getResolvedIvyPropertiesInCache(ModuleRevisionId mrid) {
+            return real.getResolvedIvyPropertiesInCache(staticMrid);
+        }
+
+        public File getConfigurationResolveReportInCache(String resolveId, String conf) {
+            return new File("build/cache/fake-default.xml");
+        }
+
+        public File[] getConfigurationResolveReportsInCache(final String resolveId) {
+            return real.getConfigurationResolveReportsInCache(RESOLVE_ID);
+        }
+
+        public ModuleDescriptor getResolvedModuleDescriptor(ModuleRevisionId mrid)
+            throws ParseException, IOException {
+            if (mrid.getOrganisation().equals("fake")) {
+                DefaultModuleDescriptor md = new DefaultModuleDescriptor(staticModuleDescriptor.getParser(), staticModuleDescriptor.getResource());
+                md.setModuleRevisionId(mrid);
+                md.setPublicationDate(staticModuleDescriptor.getPublicationDate());
+                for (Configuration conf : staticModuleDescriptor.getConfigurations()) {
+                    md.addConfiguration(conf);
+                }
+                return md;
+            }
+            return real.getResolvedModuleDescriptor(mrid);
+        }
+
+        public void saveResolvedModuleDescriptor(ModuleDescriptor md) {
+        }
+
+        public void clean() {
+        }
+    }
 }
diff --git a/test/java/org/apache/ivy/plugins/repository/file/FileRepositoryTest.java b/test/java/org/apache/ivy/plugins/repository/file/FileRepositoryTest.java
new file mode 100644
index 00000000..96675f18
--- /dev/null
+++ b/test/java/org/apache/ivy/plugins/repository/file/FileRepositoryTest.java
@@ -0,0 +1,85 @@
+/*
+ *  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
+ *
+ *      https://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.ivy.plugins.repository.file;
+
+import java.io.File;
+
+import org.apache.ivy.util.FileUtil;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public class FileRepositoryTest {
+
+    private File repoDir;
+
+    @Before
+    public void setUp() throws Exception {
+        repoDir = new File("build/filerepo").getAbsoluteFile();
+        repoDir.mkdirs();
+    }
+
+    @After
+    public void tearDown() {
+        if (repoDir != null && repoDir.exists()) {
+            FileUtil.forceDelete(repoDir);
+        }
+    }
+
+    @Test
+    public void putWrites() throws Exception {
+        FileRepository fp = new FileRepository(repoDir);
+        fp.put(new File("build.xml"), "foo/bar/baz.xml", true);
+        assertTrue(new File(repoDir + "/foo/bar/baz.xml").exists());
+    }
+
+    @Test
+    public void putWontWriteOutsideBasedir() throws Exception {
+        FileRepository fp = new FileRepository(repoDir);
+        try {
+            fp.put(new File("build.xml"), "../baz.xml", true);
+            fail("should have thrown an exception");
+        } catch (IllegalArgumentException ex) {
+            // expected
+        }
+    }
+
+    @Test
+    public void getReads() throws Exception {
+        FileRepository fp = new FileRepository(repoDir);
+        fp.put(new File("build.xml"), "foo/bar/baz.xml", true);
+        fp.get("foo/bar/baz.xml", new File("build/filerepo/a.xml"));
+        assertTrue(new File(repoDir + "/a.xml").exists());
+    }
+
+    @Test
+    public void getWontReadOutsideBasedir() throws Exception {
+        FileRepository fp = new FileRepository(repoDir);
+        try {
+            fp.get("../../build.xml", new File("build/filerepo/a.xml"));
+            fail("should have thrown an exception");
+        } catch (IllegalArgumentException ex) {
+            // expected
+        }
+    }
+
+}
diff --git a/test/java/org/apache/ivy/util/IvyPatternHelperTest.java b/test/java/org/apache/ivy/util/IvyPatternHelperTest.java
index a3d54893..cee720d0 100644
--- a/test/java/org/apache/ivy/util/IvyPatternHelperTest.java
+++ b/test/java/org/apache/ivy/util/IvyPatternHelperTest.java
@@ -19,6 +19,7 @@ package org.apache.ivy.util;
 
 import static org.junit.Assert.assertEquals;
 
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
 
@@ -80,4 +81,94 @@ public class IvyPatternHelperTest {
         String pattern = "lib/([type]/)[artifact].[ext]";
         assertEquals("lib/", IvyPatternHelper.getTokenRoot(pattern));
     }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void rejectsPathTraversalInOrganisation() {
+        String pattern = "[organisation]/[artifact]-[revision].[ext]";
+        IvyPatternHelper.substitute(pattern, "../org", "module", "revision", "artifact", "type", "ext", "conf");
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void rejectsPathTraversalInOrganization() {
+        String pattern = "[organization]/[artifact]-[revision].[ext]";
+        IvyPatternHelper.substitute(pattern, "../org", "module", "revision", "artifact", "type", "ext", "conf");
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void rejectsPathTraversalInModule() {
+        String pattern = "[module]/build/archives (x86)/[type]s/[artifact]-[revision].[ext]";
+        IvyPatternHelper.substitute(pattern, "org", "..\\module", "revision", "artifact", "type", "ext", "conf");
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void rejectsPathTraversalInRevision() {
+        String pattern = "[type]s/[artifact]-[revision].[ext]";
+        IvyPatternHelper.substitute(pattern, "org", "module", "revision/..", "artifact", "type", "ext", "conf");
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void rejectsPathTraversalInArtifact() {
+        String pattern = "[type]s/[artifact]-[revision].[ext]";
+        IvyPatternHelper.substitute(pattern, "org", "module", "revision", "artifact\\..", "type", "ext", "conf");
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void rejectsPathTraversalInType() {
+        String pattern = "[type]s/[artifact]-[revision].[ext]";
+        IvyPatternHelper.substitute(pattern, "org", "module", "revision", "artifact", "ty/../pe", "ext", "conf");
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void rejectsPathTraversalInExt() {
+        String pattern = "[type]s/[artifact]-[revision].[ext]";
+        IvyPatternHelper.substitute(pattern, "org", "module", "revision", "artifact", "type", "ex//..//t", "conf");
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void rejectsPathTraversalInConf() {
+        String pattern = "[conf]/[artifact]-[revision].[ext]";
+        IvyPatternHelper.substitute(pattern, "org", "module", "revision", "artifact", "type", "ext", "co\\..\\nf");
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void rejectsPathTraversalInModuleAttributes() {
+        String pattern = "[foo]/[artifact]-[revision].[ext]";
+        Map<String, String> a = new HashMap<String, String>() {{
+            put("foo", "..");
+        }};
+        IvyPatternHelper.substitute(pattern, "org", "module", "revision", "artifact", "type", "ext", "conf",
+            a, Collections.emptyMap());
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void rejectsPathTraversalInArtifactAttributes() {
+        String pattern = "[foo]/[artifact]-[revision].[ext]";
+        Map<String, String> a = new HashMap<String, String>() {{
+            put("foo", "a/../b");
+        }};
+        IvyPatternHelper.substitute(pattern, "org", "module", "revision", "artifact", "type", "ext", "conf",
+            Collections.emptyMap(), a);
+    }
+
+
+    @Test
+    public void ignoresPathTraversalInCoordinatesNotUsedInPatern() {
+        String pattern = "abc";
+        Map<String, String> a = new HashMap<String, String>() {{
+            put("foo", "a/../b");
+        }};
+        assertEquals("abc",
+            IvyPatternHelper.substitute(pattern, "../org", "../module", "../revision", "../artifact", "../type", "../ext", "../conf",
+                a, a)
+        );
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void rejectsPathTraversalWithoutExplicitDoubleDot() {
+        String pattern = "root/[conf]/[artifact]-[revision].[ext]";
+        // forms revision/../ext after substitution
+        IvyPatternHelper.substitute(pattern, "org", "module", "revision/", "artifact", "type", "./ext", "conf");
+    }
+
+
 }


[ant-ivy] 01/03: CVE-2022-37865 ZipPacking allows overwriting arbitrary files

Posted by bo...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

bodewig pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ant-ivy.git

commit 03b6b8c3ae27406fadb3b3539b51294af246aafa
Author: Stefan Bodewig <bo...@apache.org>
AuthorDate: Sun Aug 7 21:01:12 2022 +0200

    CVE-2022-37865 ZipPacking allows overwriting arbitrary files
---
 src/java/org/apache/ivy/core/pack/ZipPacking.java  |  11 +++-
 src/java/org/apache/ivy/util/FileUtil.java         |  62 ++++++++++++++++++
 test/java/org/apache/ivy/ant/FileUtilTest.java     |  72 +++++++++++++++++++++
 .../org/apache/ivy/core/pack/ZipPackingTest.java   |  72 +++++++++++++++++++++
 test/zip/test.zip                                  | Bin 0 -> 554 bytes
 5 files changed, 215 insertions(+), 2 deletions(-)

diff --git a/src/java/org/apache/ivy/core/pack/ZipPacking.java b/src/java/org/apache/ivy/core/pack/ZipPacking.java
index 18a010ec..197463c4 100644
--- a/src/java/org/apache/ivy/core/pack/ZipPacking.java
+++ b/src/java/org/apache/ivy/core/pack/ZipPacking.java
@@ -52,8 +52,15 @@ public class ZipPacking extends ArchivePacking {
         try (ZipInputStream zip = new ZipInputStream(packed)) {
             ZipEntry entry = null;
             while (((entry = zip.getNextEntry()) != null)) {
-                File f = new File(dest, entry.getName());
-                Message.verbose("\t\texpanding " + entry.getName() + " to " + f);
+                String entryName = entry.getName();
+                File f = FileUtil.resolveFile(dest, entryName);
+                if (!FileUtil.isLeadingPath(dest, f, true)) {
+                    Message.verbose("\t\tskipping " + entryName + " as its target "
+                                    + f.getCanonicalPath()
+                                    + " is outside of " + dest.getCanonicalPath() + ".");
+                    continue;
+                }
+                Message.verbose("\t\texpanding " + entryName + " to " + f);
 
                 // create intermediary directories - sometimes zip don't add them
                 File dirF = f.getParentFile();
diff --git a/src/java/org/apache/ivy/util/FileUtil.java b/src/java/org/apache/ivy/util/FileUtil.java
index e5e31d5a..4d387412 100644
--- a/src/java/org/apache/ivy/util/FileUtil.java
+++ b/src/java/org/apache/ivy/util/FileUtil.java
@@ -610,6 +610,68 @@ public final class FileUtil {
         return new DissectedPath(File.separator, pathToDissect);
     }
 
+    /**
+     * Learn whether one path "leads" another.
+     *
+     * <p>This method uses {@link #normalize} under the covers and
+     * does not resolve symbolic links.</p>
+     *
+     * <p>If either path tries to go beyond the file system root
+     * (i.e. it contains more ".." segments than can be travelled up)
+     * the method will return false.</p>
+     *
+     * @param leading The leading path, must not be null, must be absolute.
+     * @param path The path to check, must not be null, must be absolute.
+     * @return true if path starts with leading; false otherwise.
+     * @since Ant 1.7
+     */
+    public static boolean isLeadingPath(File leading, File path) {
+        String l = normalize(leading.getAbsolutePath()).getAbsolutePath();
+        String p = normalize(path.getAbsolutePath()).getAbsolutePath();
+        if (l.equals(p)) {
+            return true;
+        }
+        // ensure that l ends with a /
+        // so we never think /foo was a parent directory of /foobar
+        if (!l.endsWith(File.separator)) {
+            l += File.separator;
+        }
+        // ensure "/foo/"  is not considered a parent of "/foo/../../bar"
+        String up = File.separator + ".." + File.separator;
+        if (l.contains(up) || p.contains(up) || (p + File.separator).contains(up)) {
+            return false;
+        }
+        return p.startsWith(l);
+    }
+
+    /**
+     * Learn whether one path "leads" another.
+     *
+     * @param leading The leading path, must not be null, must be absolute.
+     * @param path The path to check, must not be null, must be absolute.
+     * @param resolveSymlinks whether symbolic links shall be resolved
+     * prior to comparing the paths.
+     * @return true if path starts with leading; false otherwise.
+     * @since Ant 1.9.13
+     * @throws IOException if resolveSymlinks is true and invoking
+     * getCanonicaPath on either argument throws an exception
+     */
+    public static boolean isLeadingPath(File leading, File path, boolean resolveSymlinks)
+        throws IOException {
+        if (!resolveSymlinks) {
+            return isLeadingPath(leading, path);
+        }
+        final File l = leading.getCanonicalFile();
+        File p = path.getCanonicalFile();
+        do {
+            if (l.equals(p)) {
+                return true;
+            }
+            p = p.getParentFile();
+        } while (p != null);
+        return false;
+    }
+
     /**
      * Get the length of the file, or the sum of the children lengths if it is a directory
      *
diff --git a/test/java/org/apache/ivy/ant/FileUtilTest.java b/test/java/org/apache/ivy/ant/FileUtilTest.java
index ca96483c..dd1131c1 100644
--- a/test/java/org/apache/ivy/ant/FileUtilTest.java
+++ b/test/java/org/apache/ivy/ant/FileUtilTest.java
@@ -45,6 +45,7 @@ import static org.junit.Assert.assertTrue;
 public class FileUtilTest {
 
     private static boolean symlinkCapable = false;
+    private static final String PATH_SEP = System.getProperty("path.separator");
 
     @BeforeClass
     public static void beforeClass() {
@@ -151,4 +152,75 @@ public class FileUtilTest {
         Assert.assertTrue("Unexpected content in dest file " + destFile, Arrays.equals(fileContent, Files.readAllBytes(destFile)));
     }
 
+    /**
+     * @see "https://bz.apache.org/bugzilla/show_bug.cgi?id=62502"
+     */
+    @Test
+    public void isLeadingPathCannotBeFooledByTooManyDoubleDots() {
+        Assert.assertFalse(FileUtil.isLeadingPath(new File("/foo"), new File("/foo/../../bar")));
+        Assert.assertFalse(FileUtil.isLeadingPath(new File("c:\\foo"), new File("c:\\foo\\..\\..\\bar")));
+        Assert.assertFalse(FileUtil.isLeadingPath(new File("/foo"), new File("/foo/../..")));
+    }
+
+    /**
+     * @see "https://bz.apache.org/bugzilla/show_bug.cgi?id=62502"
+     */
+    @Test
+    public void isLeadingPathCanonicalVersionCannotBeFooledByTooManyDoubleDots() throws IOException {
+        Assert.assertFalse(FileUtil.isLeadingPath(new File("/foo"), new File("/foo/../../bar"), true));
+        Assert.assertFalse(FileUtil.isLeadingPath(new File("c:\\foo"), new File("c:\\foo\\..\\..\\bar"), true));
+        Assert.assertFalse(FileUtil.isLeadingPath(new File("/foo"), new File("/foo/../.."), true));
+    }
+
+    @Test
+    public void isLeadingPathCanonicalVersionWorksAsExpectedOnUnix() throws IOException {
+        Assume.assumeFalse("Test doesn't run on DOS", PATH_SEP.equals(";"));
+        Assert.assertTrue(FileUtil.isLeadingPath(new File("/foo"), new File("/foo/bar"), true));
+        Assert.assertTrue(FileUtil.isLeadingPath(new File("/foo"), new File("/foo/baz/../bar"), true));
+        Assert.assertTrue(FileUtil.isLeadingPath(new File("/foo"), new File("/foo/../foo/bar"), true));
+        Assert.assertFalse(FileUtil.isLeadingPath(new File("/foo"), new File("/foobar"), true));
+        Assert.assertFalse(FileUtil.isLeadingPath(new File("/foo"), new File("/bar"), true));
+    }
+
+    @Test
+    public void isLeadingPathAndTrailingSlashesOnUnix() throws IOException {
+        Assume.assumeFalse("Test doesn't run on DOS", PATH_SEP.equals(";"));
+        Assert.assertTrue(FileUtil.isLeadingPath(new File("/foo/"), new File("/foo/bar"), true));
+        Assert.assertTrue(FileUtil.isLeadingPath(new File("/foo/"), new File("/foo/bar/"), true));
+        Assert.assertTrue(FileUtil.isLeadingPath(new File("/foo/"), new File("/foo/"), true));
+        Assert.assertTrue(FileUtil.isLeadingPath(new File("/foo/"), new File("/foo"), true));
+        Assert.assertTrue(FileUtil.isLeadingPath(new File("/foo"), new File("/foo/"), true));
+
+        Assert.assertTrue(FileUtil.isLeadingPath(new File("/foo/"), new File("/foo/bar"), false));
+        Assert.assertTrue(FileUtil.isLeadingPath(new File("/foo/"), new File("/foo/bar/"), false));
+        Assert.assertTrue(FileUtil.isLeadingPath(new File("/foo/"), new File("/foo/"), false));
+        Assert.assertTrue(FileUtil.isLeadingPath(new File("/foo/"), new File("/foo"), false));
+        Assert.assertTrue(FileUtil.isLeadingPath(new File("/foo"), new File("/foo/"), false));
+    }
+
+    @Test
+    public void isLeadingPathCanonicalVersionWorksAsExpectedOnDos() throws IOException {
+        Assume.assumeTrue("Test only runs on DOS", PATH_SEP.equals(";"));
+        Assert.assertTrue(FileUtil.isLeadingPath(new File("C:\\foo"), new File("C:\\foo\\bar"), true));
+        Assert.assertTrue(FileUtil.isLeadingPath(new File("C:\\foo"), new File("C:\\foo\\baz\\..\\bar"), true));
+        Assert.assertTrue(FileUtil.isLeadingPath(new File("C:\\foo"), new File("C:\\foo\\..\\foo\\bar"), true));
+        Assert.assertFalse(FileUtil.isLeadingPath(new File("C:\\foo"), new File("C:\\foobar"), true));
+        Assert.assertFalse(FileUtil.isLeadingPath(new File("C:\\foo"), new File("C:\\bar"), true));
+    }
+
+    @Test
+    public void isLeadingPathAndTrailingSlashesOnDos() throws IOException {
+        Assume.assumeTrue("Test only runs on DOS", PATH_SEP.equals(";"));
+        Assert.assertTrue(FileUtil.isLeadingPath(new File("c:\\foo\\"), new File("c:\\foo\\bar"), true));
+        Assert.assertTrue(FileUtil.isLeadingPath(new File("c:\\foo\\"), new File("c:\\foo\\bar\\"), true));
+        Assert.assertTrue(FileUtil.isLeadingPath(new File("c:\\foo\\"), new File("c:\\foo\\"), true));
+        Assert.assertTrue(FileUtil.isLeadingPath(new File("c:\\foo\\"), new File("c:\\foo"), true));
+        Assert.assertTrue(FileUtil.isLeadingPath(new File("c:\\foo"), new File("c:\\foo\\"), true));
+
+        Assert.assertTrue(FileUtil.isLeadingPath(new File("c:\\foo\\"), new File("c:\\foo\\bar"), false));
+        Assert.assertTrue(FileUtil.isLeadingPath(new File("c:\\foo\\"), new File("c:\\foo\\bar\\"), false));
+        Assert.assertTrue(FileUtil.isLeadingPath(new File("c:\\foo\\"), new File("c:\\foo\\"), false));
+        Assert.assertTrue(FileUtil.isLeadingPath(new File("c:\\foo\\"), new File("c:\\foo"), false));
+        Assert.assertTrue(FileUtil.isLeadingPath(new File("c:\\foo"), new File("c:\\foo\\"), false));
+    }
 }
diff --git a/test/java/org/apache/ivy/core/pack/ZipPackingTest.java b/test/java/org/apache/ivy/core/pack/ZipPackingTest.java
new file mode 100644
index 00000000..5435dff4
--- /dev/null
+++ b/test/java/org/apache/ivy/core/pack/ZipPackingTest.java
@@ -0,0 +1,72 @@
+/*
+ *  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
+ *
+ *      https://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.ivy.core.pack;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertFalse;
+
+import org.apache.ivy.TestHelper;
+import org.apache.ivy.util.DefaultMessageLogger;
+import org.apache.ivy.util.FileUtil;
+import org.apache.ivy.util.Message;
+import org.apache.tools.ant.Project;
+import org.apache.tools.ant.taskdefs.Mkdir;
+import org.apache.tools.ant.taskdefs.Delete;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class ZipPackingTest {
+
+    private static final Project PROJECT = TestHelper.newProject();
+    private static final File TEST_DIR = PROJECT.resolveFile("build/test/pack");
+
+    @Before
+    public void setUp() {
+        Mkdir mkdir = new Mkdir();
+        mkdir.setProject(PROJECT);
+        mkdir.setDir(TEST_DIR);
+        mkdir.execute();
+        Message.setDefaultLogger(new DefaultMessageLogger(Message.MSG_INFO));
+    }
+
+    @After
+    public void tearDown() {
+        Delete del = new Delete();
+        del.setProject(PROJECT);
+        del.setDir(TEST_DIR);
+        del.execute();
+    }
+
+    @Test
+    public void zipPackingExtractsArchive() throws IOException {
+        try (InputStream zip = new FileInputStream(PROJECT.resolveFile("test/zip/test.zip"))) {
+            new ZipPacking().unpack(zip, TEST_DIR);
+        }
+        assertTrue("Expecting file a", FileUtil.resolveFile(TEST_DIR, "a").isFile());
+        assertTrue("Expecting directory b", FileUtil.resolveFile(TEST_DIR, "b").isDirectory());
+        assertTrue("Expecting file b/c", FileUtil.resolveFile(TEST_DIR, "b/c").isFile());
+        assertTrue("Expecting directory d", FileUtil.resolveFile(TEST_DIR, "d").isDirectory());
+        assertFalse("Not expecting file e", PROJECT.resolveFile("build/test/e").exists());
+    }
+}
diff --git a/test/zip/test.zip b/test/zip/test.zip
new file mode 100644
index 00000000..b1b653a7
Binary files /dev/null and b/test/zip/test.zip differ