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");
+ }
+
+
}