diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/LocalPathPrefixComposerFactorySupport.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/LocalPathPrefixComposerFactorySupport.java index a01b377c83..6d320c9f3a 100644 --- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/LocalPathPrefixComposerFactorySupport.java +++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/LocalPathPrefixComposerFactorySupport.java @@ -24,6 +24,7 @@ import org.eclipse.aether.repository.RemoteRepository; import org.eclipse.aether.repository.RepositoryKeyFunction; import org.eclipse.aether.util.ConfigUtils; +import org.eclipse.aether.util.PathUtils; /** * Support class for {@link LocalPathPrefixComposerFactory} implementations: it predefines and makes re-usable @@ -286,13 +287,13 @@ public String getPathPrefixForRemoteArtifact(Artifact artifact, RemoteRepository } String result = remotePrefix; if (!splitRemoteRepositoryLast && splitRemoteRepository) { - result += "/" + repositoryKeyFunction.apply(repository, null); + result += "/" + repositoryKeySegment(repository); } if (splitRemote) { result += "/" + (artifact.isSnapshot() ? snapshotsPrefix : releasesPrefix); } if (splitRemoteRepositoryLast && splitRemoteRepository) { - result += "/" + repositoryKeyFunction.apply(repository, null); + result += "/" + repositoryKeySegment(repository); } return result; } @@ -316,13 +317,13 @@ public String getPathPrefixForRemoteMetadata(Metadata metadata, RemoteRepository } String result = remotePrefix; if (!splitRemoteRepositoryLast && splitRemoteRepository) { - result += "/" + repositoryKeyFunction.apply(repository, null); + result += "/" + repositoryKeySegment(repository); } if (splitRemote) { result += "/" + (isSnapshot(metadata) ? snapshotsPrefix : releasesPrefix); } if (splitRemoteRepositoryLast && splitRemoteRepository) { - result += "/" + repositoryKeyFunction.apply(repository, null); + result += "/" + repositoryKeySegment(repository); } return result; } @@ -330,5 +331,15 @@ public String getPathPrefixForRemoteMetadata(Metadata metadata, RemoteRepository protected boolean isSnapshot(Metadata metadata) { return !metadata.getVersion().isEmpty() && metadata.getVersion().endsWith("-SNAPSHOT"); } + + /** + * Defense in depth: the repository key is spliced into the local repository path prefix, so it + * must be a safe path segment. + */ + private String repositoryKeySegment(RemoteRepository repository) { + String key = repositoryKeyFunction.apply(repository, null); + PathUtils.validatePathComponent(key, "repository key"); + return key; + } } } diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/SparseDirectoryTrustedChecksumsSource.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/SparseDirectoryTrustedChecksumsSource.java index 0456b3f179..8094374ca1 100644 --- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/SparseDirectoryTrustedChecksumsSource.java +++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/checksum/SparseDirectoryTrustedChecksumsSource.java @@ -39,6 +39,7 @@ import org.eclipse.aether.spi.io.ChecksumProcessor; import org.eclipse.aether.spi.remoterepo.RepositoryKeyFunctionFactory; import org.eclipse.aether.util.ConfigUtils; +import org.eclipse.aether.util.PathUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -178,6 +179,9 @@ private String calculateArtifactPath( String path = localPathComposer.getPathForArtifact(artifact, false) + "." + checksumAlgorithmFactory.getFileExtension(); if (originAware) { + // defense in depth: the repository key is spliced into a path under the checksums basedir, + // so it must be a safe path segment + PathUtils.validatePathComponent(safeRepositoryId, "repository key"); path = safeRepositoryId + "/" + path; } return path; diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultLocalPathPrefixComposerFactoryTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultLocalPathPrefixComposerFactoryTest.java index 8f6c53452f..a0ecc3c696 100644 --- a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultLocalPathPrefixComposerFactoryTest.java +++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultLocalPathPrefixComposerFactoryTest.java @@ -232,4 +232,30 @@ void fullConfig() { assertNotNull(prefix); assertEquals("cached/my-repo/releases", prefix); } + + @Test + void dotDotRepositoryIdIsNeutralizedInSplitPrefix() { + DefaultRepositorySystemSession session = TestUtils.newSession(); + session.setConfigProperty(DefaultLocalPathPrefixComposerFactory.CONFIG_PROP_SPLIT, Boolean.TRUE.toString()); + session.setConfigProperty( + DefaultLocalPathPrefixComposerFactory.CONFIG_PROP_SPLIT_REMOTE_REPOSITORY, Boolean.TRUE.toString()); + + LocalPathPrefixComposerFactory factory = + new DefaultLocalPathPrefixComposerFactory(new DefaultRepositoryKeyFunctionFactory()); + LocalPathPrefixComposer composer = factory.createComposer(session); + assertNotNull(composer); + + // a repository id of ".." must be neutralized into a harmless path segment rather than spliced + // into the prefix as-is + RemoteRepository dotDotRepository = + new RemoteRepository.Builder("..", "default", "https://repo.example.org/").build(); + + String prefix = composer.getPathPrefixForRemoteArtifact(releaseArtifact, dotDotRepository); + assertNotNull(prefix); + assertEquals("cached/-DOTDOT-", prefix); + + prefix = composer.getPathPrefixForRemoteMetadata(releaseMetadata, dotDotRepository); + assertNotNull(prefix); + assertEquals("cached/-DOTDOT-", prefix); + } } diff --git a/maven-resolver-util/src/main/java/org/eclipse/aether/util/PathUtils.java b/maven-resolver-util/src/main/java/org/eclipse/aether/util/PathUtils.java index b853ae1113..f44c28b38a 100644 --- a/maven-resolver-util/src/main/java/org/eclipse/aether/util/PathUtils.java +++ b/maven-resolver-util/src/main/java/org/eclipse/aether/util/PathUtils.java @@ -73,7 +73,16 @@ public static String stringToPathSegment(String string) { pos = result.indexOf(illegal); } } - return result.toString(); + // Strings consisting solely of dots contain no illegal character, yet "." and ".." carry path + // meaning when used as a path segment. Map them to explicit tokens, mirroring the character + // replacements above. + String segment = result.toString(); + if (segment.equals(".")) { + return "-DOT-"; + } else if (segment.equals("..")) { + return "-DOTDOT-"; + } + return segment; } /** diff --git a/maven-resolver-util/src/test/java/org/eclipse/aether/util/PathUtilsTest.java b/maven-resolver-util/src/test/java/org/eclipse/aether/util/PathUtilsTest.java index 97c1b45631..a38d4ef935 100644 --- a/maven-resolver-util/src/test/java/org/eclipse/aether/util/PathUtilsTest.java +++ b/maven-resolver-util/src/test/java/org/eclipse/aether/util/PathUtilsTest.java @@ -40,6 +40,18 @@ void stringToPathSegment_fixes() { assertEquals("bad-COLON-id", badFixedId); } + @Test + void stringToPathSegment_dotSegments() { + // "." and ".." contain no illegal character, but carry path meaning when used as a path segment + assertEquals("-DOTDOT-", PathUtils.stringToPathSegment("..")); + assertEquals("-DOT-", PathUtils.stringToPathSegment(".")); + // dotted names and longer dot runs are inert as single path segments and stay untouched + assertEquals("...", PathUtils.stringToPathSegment("...")); + assertEquals("my.repo", PathUtils.stringToPathSegment("my.repo")); + assertEquals("repo..id", PathUtils.stringToPathSegment("repo..id")); + assertEquals("..id", PathUtils.stringToPathSegment("..id")); + } + @Test void stringToPathSegment_allCharsBad() { String veryBad = "\\/:\"<>|?*";