CVE-2022-37866 prevent path-traversal with bogus module coordinates
diff --git a/src/java/org/apache/ivy/core/IvyPatternHelper.java b/src/java/org/apache/ivy/core/IvyPatternHelper.java
index 3dacb7d..3614ac7 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.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 @@
                 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 @@
                 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 @@
                     + 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 @@
         }
         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 497a536..234810a 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 @@
     }
 
     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 @@
      */
     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 @@
                                         + resourceResolver
                                         + "': pointing repository to ivy cache is forbidden !");
                             }
+                            assertInsideCache(archiveFile);
                             if (listener != null) {
                                 listener.startArtifactDownload(this, artifactRef, artifact, origin);
                             }
@@ -1147,6 +1151,7 @@
                         }
 
                         // actual download
+                        assertInsideCache(archiveFile);
                         if (archiveFile.exists()) {
                             archiveFile.delete();
                         }
@@ -1535,6 +1540,16 @@
     }
 
     /**
+     * @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
      * path of the file it represents. Else returns back {@link ArtifactOrigin#getLocation()}
diff --git a/src/java/org/apache/ivy/core/cache/DefaultResolutionCacheManager.java b/src/java/org/apache/ivy/core/cache/DefaultResolutionCacheManager.java
index 52c3400..e901f1f 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 @@
             IOException {
         ModuleRevisionId mrevId = md.getResolvedModuleRevisionId();
         File ivyFileInCache = getResolvedIvyFileInCache(mrevId);
+        assertInsideCache(ivyFileInCache);
         md.toIvyFile(ivyFileInCache);
 
         Properties paths = new Properties();
@@ -188,12 +189,22 @@
         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 @@
             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 7333e32..d746bb0 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.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 @@
             // 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 d50f047..b6709ff 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 @@
         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 @@
                 }
 
                 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 @@
                     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 c4a31f3..7625102 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.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 @@
             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 fa13de7..5de1bb3 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 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 @@
         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 48ccd1e..b88886d 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 @@
 
 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.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;
@@ -139,6 +146,57 @@
     }
 
     @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 {
         // given a module org#module
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 0000000..45a9c7d
--- /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 243e411..d4251ed 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 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 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 @@
                     "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 e3157f1..742202a 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 @@
 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.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.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.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 public class RetrieveTest {
 
@@ -136,6 +144,47 @@
     }
 
     @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
         ResolveReport report = ivy.resolve(new File(
@@ -610,4 +659,58 @@
         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 0000000..96675f1
--- /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 a3d5489..cee720d 100644
--- a/test/java/org/apache/ivy/util/IvyPatternHelperTest.java
+++ b/test/java/org/apache/ivy/util/IvyPatternHelperTest.java
@@ -19,6 +19,7 @@
 
 import static org.junit.Assert.assertEquals;
 
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
 
@@ -80,4 +81,94 @@
         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");
+    }
+
+
 }