CVE-2022-37866 prevent path-traversal with bogus module coordinates

This commit is contained in:
Stefan Bodewig 2022-08-21 18:54:43 +02:00
parent 03b6b8c3ae
commit 3f374602d4
13 changed files with 559 additions and 21 deletions

View File

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

View File

@ -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

View File

@ -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(),

View File

@ -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<>();

View File

@ -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) {

View File

@ -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);

View File

@ -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() {

View File

@ -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 {

View File

@ -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
}
}
}

View File

@ -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.

View File

@ -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() {
}
}
}

View File

@ -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
}
}
}

View File

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