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 <[email protected]> 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"); + } + + }
