diff --git a/maven-compat/src/main/java/org/apache/maven/artifact/repository/metadata/DefaultRepositoryMetadataManager.java b/maven-compat/src/main/java/org/apache/maven/artifact/repository/metadata/DefaultRepositoryMetadataManager.java
index 14f73e12907d..0169406bea76 100644
--- a/maven-compat/src/main/java/org/apache/maven/artifact/repository/metadata/DefaultRepositoryMetadataManager.java
+++ b/maven-compat/src/main/java/org/apache/maven/artifact/repository/metadata/DefaultRepositoryMetadataManager.java
@@ -273,6 +273,8 @@ protected Metadata readMetadata(File mappingFile) throws RepositoryMetadataReadE
ValidatingMetadataXpp3Reader mappingReader = new ValidatingMetadataXpp3Reader();
result = mappingReader.read(reader, false);
+
+ validateVersioning(result);
} catch (FileNotFoundException e) {
throw new RepositoryMetadataReadException("Cannot read metadata from '" + mappingFile + "'", e);
} catch (IOException | XmlPullParserException e) {
@@ -282,6 +284,51 @@ protected Metadata readMetadata(File mappingFile) throws RepositoryMetadataReadE
return result;
}
+ /**
+ * Version tokens adopted from repository metadata must be valid coordinate components; metadata carrying
+ * anything else is treated as invalid.
+ */
+ private static void validateVersioning(Metadata metadata) throws RepositoryMetadataReadException {
+ if (metadata == null) {
+ return;
+ }
+ Versioning versioning = metadata.getVersioning();
+ if (versioning == null) {
+ return;
+ }
+ validateVersionToken(versioning.getLatest());
+ validateVersionToken(versioning.getRelease());
+ for (String version : versioning.getVersions()) {
+ validateVersionToken(version);
+ }
+ for (SnapshotVersion snapshotVersion : versioning.getSnapshotVersions()) {
+ validateVersionToken(snapshotVersion.getVersion());
+ }
+ Snapshot snapshot = versioning.getSnapshot();
+ if (snapshot != null) {
+ validateVersionToken(snapshot.getTimestamp());
+ }
+ }
+
+ private static void validateVersionToken(String value) throws RepositoryMetadataReadException {
+ if (value == null || value.isEmpty()) {
+ return;
+ }
+ boolean valid = !"..".equals(value);
+ if (valid) {
+ for (int i = 0; i < value.length(); i++) {
+ char c = value.charAt(i);
+ if (c == '/' || c == '\\' || c == ':' || Character.isISOControl(c)) {
+ valid = false;
+ break;
+ }
+ }
+ }
+ if (!valid) {
+ throw new RepositoryMetadataReadException("Metadata contains an invalid version token: '" + value + "'");
+ }
+ }
+
/**
* Ensures the last updated timestamp of the specified metadata does not refer to the future and fixes the local
* metadata if necessary to allow proper merging/updating of metadata during deployment.
diff --git a/maven-compat/src/test/java/org/apache/maven/artifact/repository/metadata/DefaultRepositoryMetadataManagerTest.java b/maven-compat/src/test/java/org/apache/maven/artifact/repository/metadata/DefaultRepositoryMetadataManagerTest.java
new file mode 100644
index 000000000000..b56dc2be6546
--- /dev/null
+++ b/maven-compat/src/test/java/org/apache/maven/artifact/repository/metadata/DefaultRepositoryMetadataManagerTest.java
@@ -0,0 +1,63 @@
+/*
+ * 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
+ *
+ * http://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.maven.artifact.repository.metadata;
+
+import java.io.File;
+import java.net.URL;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Tests that {@link DefaultRepositoryMetadataManager} rejects repository metadata carrying version tokens that
+ * are not valid coordinate components, on the legacy read path used when metadata is loaded for merging.
+ */
+public class DefaultRepositoryMetadataManagerTest {
+
+ private final DefaultRepositoryMetadataManager manager = new DefaultRepositoryMetadataManager();
+
+ @Test
+ void testMetadataWithInvalidVersionTokenIsRejected() {
+ File metadataFile = testFile("metadata-invalid-token/maven-metadata.xml");
+
+ RepositoryMetadataReadException exception =
+ assertThrows(RepositoryMetadataReadException.class, () -> manager.readMetadata(metadataFile));
+
+ assertTrue(exception.getMessage().contains("invalid version token"), exception.getMessage());
+ }
+
+ @Test
+ void testMetadataWithInvalidSnapshotTimestampIsRejected() {
+ File metadataFile = testFile("metadata-invalid-timestamp/maven-metadata.xml");
+
+ RepositoryMetadataReadException exception =
+ assertThrows(RepositoryMetadataReadException.class, () -> manager.readMetadata(metadataFile));
+
+ assertTrue(exception.getMessage().contains("invalid version token"), exception.getMessage());
+ }
+
+ private static File testFile(String resource) {
+ URL url = Thread.currentThread().getContextClassLoader().getResource(resource);
+ assertNotNull(url, "test resource not found: " + resource);
+ return new File(url.getFile());
+ }
+}
diff --git a/maven-compat/src/test/resources/metadata-invalid-timestamp/maven-metadata.xml b/maven-compat/src/test/resources/metadata-invalid-timestamp/maven-metadata.xml
new file mode 100644
index 000000000000..fdd7cc27fdfb
--- /dev/null
+++ b/maven-compat/src/test/resources/metadata-invalid-timestamp/maven-metadata.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+ org.apache.maven.its
+ dep-invalid-timestamp
+ 1.0-SNAPSHOT
+
+
+ 20120809.112920:1
+ 1
+
+ 20120809112920
+
+
diff --git a/maven-compat/src/test/resources/metadata-invalid-token/maven-metadata.xml b/maven-compat/src/test/resources/metadata-invalid-token/maven-metadata.xml
new file mode 100644
index 000000000000..92e1b6b813d3
--- /dev/null
+++ b/maven-compat/src/test/resources/metadata-invalid-token/maven-metadata.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+ org.apache.maven.its
+ dep-invalid-token
+ 1.0-SNAPSHOT
+
+ 1.0:2.0
+ 20120809112920
+
+
diff --git a/maven-core/src/main/java/org/apache/maven/artifact/repository/metadata/io/DefaultMetadataReader.java b/maven-core/src/main/java/org/apache/maven/artifact/repository/metadata/io/DefaultMetadataReader.java
index f1ad14f5227a..d4a94851b788 100644
--- a/maven-core/src/main/java/org/apache/maven/artifact/repository/metadata/io/DefaultMetadataReader.java
+++ b/maven-core/src/main/java/org/apache/maven/artifact/repository/metadata/io/DefaultMetadataReader.java
@@ -29,6 +29,10 @@
import java.util.Objects;
import org.apache.maven.artifact.repository.metadata.Metadata;
+import org.apache.maven.artifact.repository.metadata.Plugin;
+import org.apache.maven.artifact.repository.metadata.Snapshot;
+import org.apache.maven.artifact.repository.metadata.SnapshotVersion;
+import org.apache.maven.artifact.repository.metadata.Versioning;
import org.apache.maven.repository.internal.metadata.ValidatingMetadataXpp3Reader;
import org.codehaus.plexus.util.ReaderFactory;
import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
@@ -54,7 +58,9 @@ public Metadata read(Reader input, Map options) throws IOException {
Objects.requireNonNull(input, "input cannot be null");
try (Reader in = input) {
- return new ValidatingMetadataXpp3Reader().read(in, isStrict(options));
+ Metadata metadata = new ValidatingMetadataXpp3Reader().read(in, isStrict(options));
+ validateMetadata(metadata);
+ return metadata;
} catch (XmlPullParserException e) {
throw new MetadataParseException(e.getMessage(), e.getLineNumber(), e.getColumnNumber(), e);
}
@@ -64,7 +70,9 @@ public Metadata read(InputStream input, Map options) throws IOExcepti
Objects.requireNonNull(input, "input cannot be null");
try (InputStream in = input) {
- return new ValidatingMetadataXpp3Reader().read(in, isStrict(options));
+ Metadata metadata = new ValidatingMetadataXpp3Reader().read(in, isStrict(options));
+ validateMetadata(metadata);
+ return metadata;
} catch (XmlPullParserException e) {
throw new MetadataParseException(e.getMessage(), e.getLineNumber(), e.getColumnNumber(), e);
}
@@ -74,4 +82,57 @@ private boolean isStrict(Map options) {
Object value = (options != null) ? options.get(IS_STRICT) : null;
return value == null || Boolean.parseBoolean(value.toString());
}
+
+ /**
+ * Coordinate-shaped tokens read from this metadata (versions, plugin artifactIds and prefixes) get carried
+ * forward by callers as if they were already-validated path and coordinate components. Reject anything that
+ * would not itself be a valid coordinate component here, before it leaves this reader.
+ */
+ private static void validateMetadata(Metadata metadata) throws IOException {
+ if (metadata == null) {
+ return;
+ }
+
+ Versioning versioning = metadata.getVersioning();
+ if (versioning != null) {
+ validateToken("version", versioning.getRelease());
+ validateToken("version", versioning.getLatest());
+ for (String version : versioning.getVersions()) {
+ validateToken("version", version);
+ }
+ for (SnapshotVersion snapshotVersion : versioning.getSnapshotVersions()) {
+ validateToken("version", snapshotVersion.getVersion());
+ }
+ Snapshot snapshot = versioning.getSnapshot();
+ if (snapshot != null) {
+ validateToken("snapshot timestamp", snapshot.getTimestamp());
+ }
+ }
+
+ if (metadata.getPlugins() != null) {
+ for (Plugin plugin : metadata.getPlugins()) {
+ validateToken("plugin artifactId", plugin.getArtifactId());
+ validateToken("plugin prefix", plugin.getPrefix());
+ }
+ }
+ }
+
+ private static void validateToken(String field, String value) throws IOException {
+ if (value == null || value.isEmpty()) {
+ return;
+ }
+ boolean valid = !"..".equals(value);
+ if (valid) {
+ for (int i = 0; i < value.length(); i++) {
+ char c = value.charAt(i);
+ if (c == '/' || c == '\\' || c == ':' || Character.isISOControl(c)) {
+ valid = false;
+ break;
+ }
+ }
+ }
+ if (!valid) {
+ throw new IOException("Metadata contains an invalid " + field + ": '" + value + "'");
+ }
+ }
}
diff --git a/maven-core/src/test/java/org/apache/maven/artifact/repository/metadata/io/DefaultMetadataReaderTest.java b/maven-core/src/test/java/org/apache/maven/artifact/repository/metadata/io/DefaultMetadataReaderTest.java
new file mode 100644
index 000000000000..f80da9ba2ac2
--- /dev/null
+++ b/maven-core/src/test/java/org/apache/maven/artifact/repository/metadata/io/DefaultMetadataReaderTest.java
@@ -0,0 +1,67 @@
+/*
+ * 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
+ *
+ * http://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.maven.artifact.repository.metadata.io;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.util.Collections;
+
+import org.apache.maven.artifact.repository.metadata.Metadata;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class DefaultMetadataReaderTest {
+
+ private final DefaultMetadataReader reader = new DefaultMetadataReader();
+
+ private File resource(String name) throws URISyntaxException {
+ return new File(getClass().getResource(name).toURI());
+ }
+
+ @Test
+ public void testWellFormedMetadataParsesUnchanged() throws Exception {
+ Metadata metadata = reader.read(resource("well-formed-metadata.xml"), Collections.emptyMap());
+
+ assertEquals("org.apache.maven.its", metadata.getGroupId());
+ assertEquals("sample", metadata.getArtifactId());
+ assertEquals("1.1", metadata.getVersioning().getRelease());
+ assertEquals("1.1", metadata.getVersioning().getLatest());
+ assertEquals("maven-sample-plugin", metadata.getPlugins().get(0).getArtifactId());
+ }
+
+ @Test
+ public void testVersionContainingColonIsRejected() throws Exception {
+ File input = resource("invalid-version-token.xml");
+
+ IOException e = assertThrows(IOException.class, () -> reader.read(input, Collections.emptyMap()));
+ assertTrue(e.getMessage().contains("1.0:evil"));
+ }
+
+ @Test
+ public void testPluginArtifactIdContainingSlashIsRejected() throws Exception {
+ File input = resource("invalid-plugin-artifactid.xml");
+
+ IOException e = assertThrows(IOException.class, () -> reader.read(input, Collections.emptyMap()));
+ assertTrue(e.getMessage().contains("maven/sample-plugin"));
+ }
+}
diff --git a/maven-core/src/test/resources/org/apache/maven/artifact/repository/metadata/io/invalid-plugin-artifactid.xml b/maven-core/src/test/resources/org/apache/maven/artifact/repository/metadata/io/invalid-plugin-artifactid.xml
new file mode 100644
index 000000000000..aec5e1aa04f3
--- /dev/null
+++ b/maven-core/src/test/resources/org/apache/maven/artifact/repository/metadata/io/invalid-plugin-artifactid.xml
@@ -0,0 +1,31 @@
+
+
+
+ org.apache.maven.its
+ sample
+
+
+ Sample Plugin
+ sample
+
+ maven/sample-plugin
+
+
+
diff --git a/maven-core/src/test/resources/org/apache/maven/artifact/repository/metadata/io/invalid-version-token.xml b/maven-core/src/test/resources/org/apache/maven/artifact/repository/metadata/io/invalid-version-token.xml
new file mode 100644
index 000000000000..14216e28319d
--- /dev/null
+++ b/maven-core/src/test/resources/org/apache/maven/artifact/repository/metadata/io/invalid-version-token.xml
@@ -0,0 +1,28 @@
+
+
+
+ org.apache.maven.its
+ sample
+
+
+ 1.0:evil
+ 20150428055824
+
+
diff --git a/maven-core/src/test/resources/org/apache/maven/artifact/repository/metadata/io/well-formed-metadata.xml b/maven-core/src/test/resources/org/apache/maven/artifact/repository/metadata/io/well-formed-metadata.xml
new file mode 100644
index 000000000000..ce3b4fa9365a
--- /dev/null
+++ b/maven-core/src/test/resources/org/apache/maven/artifact/repository/metadata/io/well-formed-metadata.xml
@@ -0,0 +1,39 @@
+
+
+
+ org.apache.maven.its
+ sample
+
+ 1.1
+ 1.1
+
+ 1.0
+ 1.1
+
+ 20150428055824
+
+
+
+ Sample Plugin
+ sample
+ maven-sample-plugin
+
+
+
diff --git a/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/DefaultModelResolver.java b/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/DefaultModelResolver.java
index 398652003453..4dc86735a1fb 100644
--- a/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/DefaultModelResolver.java
+++ b/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/DefaultModelResolver.java
@@ -75,6 +75,8 @@ class DefaultModelResolver implements ModelResolver {
private final Set repositoryIds;
+ private final Set externalRepositoryIds;
+
DefaultModelResolver(
RepositorySystemSession session,
RequestTrace trace,
@@ -93,6 +95,11 @@ class DefaultModelResolver implements ModelResolver {
this.externalRepositories = Collections.unmodifiableList(new ArrayList<>(repositories));
this.repositoryIds = new HashSet<>();
+ Set externalIds = new HashSet<>();
+ for (RemoteRepository externalRepository : this.externalRepositories) {
+ externalIds.add(externalRepository.getId());
+ }
+ this.externalRepositoryIds = Collections.unmodifiableSet(externalIds);
}
private DefaultModelResolver(DefaultModelResolver original) {
@@ -105,6 +112,7 @@ private DefaultModelResolver(DefaultModelResolver original) {
this.repositories = new ArrayList<>(original.repositories);
this.externalRepositories = original.externalRepositories;
this.repositoryIds = new HashSet<>(original.repositoryIds);
+ this.externalRepositoryIds = original.externalRepositoryIds;
}
@Override
@@ -123,6 +131,13 @@ public void addRepository(final Repository repository, boolean replace) throws I
return;
}
+ if (externalRepositoryIds.contains(repository.getId())) {
+ // Replacement is meant to refresh a repository this model declared earlier, e.g.
+ // once its URL has been interpolated. Repositories supplied by the request or the
+ // session are not model-declared, so they keep precedence and are left in place.
+ return;
+ }
+
removeMatchingRepository(repositories, repository.getId());
}
diff --git a/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/DefaultVersionRangeResolver.java b/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/DefaultVersionRangeResolver.java
index 9542bece8977..f5e1879bdab5 100644
--- a/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/DefaultVersionRangeResolver.java
+++ b/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/DefaultVersionRangeResolver.java
@@ -23,6 +23,7 @@
import javax.inject.Singleton;
import java.io.FileInputStream;
+import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
@@ -274,9 +275,13 @@ private Versioning readVersions(
if (metadata.getFile() != null && metadata.getFile().exists()) {
try (InputStream in = new FileInputStream(metadata.getFile())) {
- versioning = new ValidatingMetadataXpp3Reader()
+ Versioning parsed = new ValidatingMetadataXpp3Reader()
.read(in, false)
.getVersioning();
+
+ validateVersioning(parsed);
+
+ versioning = parsed;
}
}
}
@@ -289,6 +294,40 @@ private Versioning readVersions(
return (versioning != null) ? versioning : new Versioning();
}
+ /**
+ * Version tokens adopted from repository metadata must be valid coordinate components; metadata carrying
+ * anything else is treated as invalid.
+ */
+ private static void validateVersioning(Versioning versioning) throws IOException {
+ if (versioning == null) {
+ return;
+ }
+ for (String version : versioning.getVersions()) {
+ validateVersionToken(version);
+ }
+ validateVersionToken(versioning.getLatest());
+ validateVersionToken(versioning.getRelease());
+ }
+
+ private static void validateVersionToken(String value) throws IOException {
+ if (value == null || value.isEmpty()) {
+ return;
+ }
+ boolean valid = !"..".equals(value);
+ if (valid) {
+ for (int i = 0; i < value.length(); i++) {
+ char c = value.charAt(i);
+ if (c == '/' || c == '\\' || c == ':' || Character.isISOControl(c)) {
+ valid = false;
+ break;
+ }
+ }
+ }
+ if (!valid) {
+ throw new IOException("Metadata contains an invalid version token: '" + value + "'");
+ }
+ }
+
private Versioning filterVersionsByRepositoryType(Versioning versioning, RemoteRepository remoteRepository) {
if (remoteRepository == null) {
return versioning;
diff --git a/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/DefaultVersionResolver.java b/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/DefaultVersionResolver.java
index b3b5fe70cac6..70b65d9b2602 100644
--- a/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/DefaultVersionResolver.java
+++ b/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/DefaultVersionResolver.java
@@ -278,10 +278,14 @@ private Versioning readVersions(
if (metadata.getFile() != null && metadata.getFile().exists()) {
try (InputStream in = new FileInputStream(metadata.getFile())) {
- versioning = new ValidatingMetadataXpp3Reader()
+ Versioning parsed = new ValidatingMetadataXpp3Reader()
.read(in, false)
.getVersioning();
+ validateVersioning(parsed);
+
+ versioning = parsed;
+
/*
NOTE: Users occasionally misuse the id "local" for remote repos which screws up the metadata
of the local repository. This is especially troublesome during snapshot resolution so we try
@@ -312,6 +316,44 @@ private Versioning readVersions(
return (versioning != null) ? versioning : new Versioning();
}
+ /**
+ * Version tokens adopted from repository metadata must be valid coordinate components; metadata carrying
+ * anything else is treated as invalid.
+ */
+ private static void validateVersioning(Versioning versioning) throws IOException {
+ if (versioning == null) {
+ return;
+ }
+ validateVersionToken(versioning.getLatest());
+ validateVersionToken(versioning.getRelease());
+ for (SnapshotVersion snapshotVersion : versioning.getSnapshotVersions()) {
+ validateVersionToken(snapshotVersion.getVersion());
+ }
+ Snapshot snapshot = versioning.getSnapshot();
+ if (snapshot != null) {
+ validateVersionToken(snapshot.getTimestamp());
+ }
+ }
+
+ private static void validateVersionToken(String value) throws IOException {
+ if (value == null || value.isEmpty()) {
+ return;
+ }
+ boolean valid = !"..".equals(value);
+ if (valid) {
+ for (int i = 0; i < value.length(); i++) {
+ char c = value.charAt(i);
+ if (c == '/' || c == '\\' || c == ':' || Character.isISOControl(c)) {
+ valid = false;
+ break;
+ }
+ }
+ }
+ if (!valid) {
+ throw new IOException("Metadata contains an invalid version token: '" + value + "'");
+ }
+ }
+
private void invalidMetadata(
RepositorySystemSession session,
RequestTrace trace,
diff --git a/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/relocation/DistributionManagementArtifactRelocationSource.java b/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/relocation/DistributionManagementArtifactRelocationSource.java
index 790e85280a98..83b1c259bb15 100644
--- a/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/relocation/DistributionManagementArtifactRelocationSource.java
+++ b/maven-resolver-provider/src/main/java/org/apache/maven/repository/internal/relocation/DistributionManagementArtifactRelocationSource.java
@@ -28,6 +28,7 @@
import org.apache.maven.repository.internal.RelocatedArtifact;
import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.resolution.ArtifactDescriptorException;
import org.eclipse.aether.resolution.ArtifactDescriptorResult;
import org.eclipse.sisu.Priority;
import org.slf4j.Logger;
@@ -50,11 +51,15 @@ public final class DistributionManagementArtifactRelocationSource implements Mav
@Override
public Artifact relocatedTarget(
- RepositorySystemSession session, ArtifactDescriptorResult artifactDescriptorResult, Model model) {
+ RepositorySystemSession session, ArtifactDescriptorResult artifactDescriptorResult, Model model)
+ throws ArtifactDescriptorException {
DistributionManagement distMgmt = model.getDistributionManagement();
if (distMgmt != null) {
Relocation relocation = distMgmt.getRelocation();
if (relocation != null) {
+ validateCoordinateComponent(relocation.getGroupId(), "groupId", artifactDescriptorResult);
+ validateCoordinateComponent(relocation.getArtifactId(), "artifactId", artifactDescriptorResult);
+ validateCoordinateComponent(relocation.getVersion(), "version", artifactDescriptorResult);
Artifact result = new RelocatedArtifact(
artifactDescriptorResult.getRequest().getArtifact(),
relocation.getGroupId(),
@@ -73,4 +78,36 @@ public Artifact relocatedTarget(
}
return null;
}
+
+ /**
+ * Checks that a relocation coordinate component is usable as an artifact coordinate. Components outside
+ * the coordinate character set are rejected so that only well-formed coordinates enter resolution.
+ */
+ private static void validateCoordinateComponent(
+ String value, String component, ArtifactDescriptorResult artifactDescriptorResult)
+ throws ArtifactDescriptorException {
+ if (value == null || value.isEmpty()) {
+ return; // component is not relocated: the original artifact's value is kept
+ }
+ if (isInvalidCoordinateComponent(value)) {
+ IllegalArgumentException cause = new IllegalArgumentException("Invalid relocation " + component + " '"
+ + value + "' in artifact descriptor for "
+ + artifactDescriptorResult.getRequest().getArtifact()
+ + ": not a valid artifact coordinate component");
+ artifactDescriptorResult.addException(cause);
+ throw new ArtifactDescriptorException(artifactDescriptorResult, cause.getMessage(), cause);
+ }
+ }
+
+ private static boolean isInvalidCoordinateComponent(String value) {
+ if ("..".equals(value) || value.contains("/") || value.contains("\\") || value.contains(":")) {
+ return true;
+ }
+ for (int i = 0; i < value.length(); i++) {
+ if (Character.isISOControl(value.charAt(i))) {
+ return true;
+ }
+ }
+ return false;
+ }
}
diff --git a/maven-resolver-provider/src/test/java/org/apache/maven/repository/internal/DefaultModelResolverTest.java b/maven-resolver-provider/src/test/java/org/apache/maven/repository/internal/DefaultModelResolverTest.java
index 747d24461f94..6b2b880ded4d 100644
--- a/maven-resolver-provider/src/test/java/org/apache/maven/repository/internal/DefaultModelResolverTest.java
+++ b/maven-resolver-provider/src/test/java/org/apache/maven/repository/internal/DefaultModelResolverTest.java
@@ -20,19 +20,25 @@
import javax.inject.Inject;
+import java.io.File;
import java.net.MalformedURLException;
+import java.nio.file.Path;
import java.util.Arrays;
import org.apache.maven.model.Dependency;
import org.apache.maven.model.Parent;
+import org.apache.maven.model.Repository;
import org.apache.maven.model.resolution.ModelResolver;
import org.apache.maven.model.resolution.UnresolvableModelException;
import org.codehaus.plexus.component.repository.exception.ComponentLookupException;
import org.codehaus.plexus.testing.PlexusTest;
+import org.eclipse.aether.DefaultRepositorySystemSession;
import org.eclipse.aether.impl.ArtifactResolver;
import org.eclipse.aether.impl.RemoteRepositoryManager;
import org.eclipse.aether.impl.VersionRangeResolver;
+import org.eclipse.aether.repository.LocalRepository;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -195,6 +201,41 @@ public void testResolveDependencySuccessfullyResolvesExistingDependencyUsingHigh
@Inject
private RemoteRepositoryManager remoteRepositoryManager;
+ @Test
+ public void testConstructionSuppliedRepositoryKeepsPrecedence(@TempDir Path localRepository) throws Exception {
+ // An empty local repository, so resolution has to consult the remote repository list
+ // rather than a copy cached by another test in this class.
+ final DefaultRepositorySystemSession isolatedSession = MavenRepositorySystemUtils.newSession();
+ isolatedSession.setLocalRepositoryManager(
+ system.newLocalRepositoryManager(isolatedSession, new LocalRepository(localRepository.toFile())));
+
+ final ModelResolver resolver = new DefaultModelResolver(
+ isolatedSession,
+ null,
+ this.getClass().getName(),
+ artifactResolver,
+ versionRangeResolver,
+ remoteRepositoryManager,
+ Arrays.asList(newTestRepository()));
+
+ // A model-declared repository that reuses the external repository's id; the external
+ // repository must keep its slot.
+ final Repository repository = new Repository();
+ repository.setId("repo");
+ repository.setUrl(new File("target/no-such-repository").toURI().toURL().toString());
+
+ resolver.addRepository(repository);
+ resolver.addRepository(repository, true);
+
+ final Parent parent = new Parent();
+ parent.setGroupId("ut.simple");
+ parent.setArtifactId("artifact");
+ parent.setVersion("1.0");
+
+ // The external repository kept its slot, so the artifact still resolves.
+ assertNotNull(resolver.resolveModel(parent));
+ }
+
private ModelResolver newModelResolver() throws ComponentLookupException, MalformedURLException {
return new DefaultModelResolver(
this.session,
diff --git a/maven-resolver-provider/src/test/java/org/apache/maven/repository/internal/DefaultVersionRangeResolverTest.java b/maven-resolver-provider/src/test/java/org/apache/maven/repository/internal/DefaultVersionRangeResolverTest.java
new file mode 100644
index 000000000000..99dd25904c3b
--- /dev/null
+++ b/maven-resolver-provider/src/test/java/org/apache/maven/repository/internal/DefaultVersionRangeResolverTest.java
@@ -0,0 +1,75 @@
+/*
+ * 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
+ *
+ * http://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.maven.repository.internal;
+
+import javax.inject.Inject;
+
+import java.nio.file.Path;
+
+import org.codehaus.plexus.testing.PlexusTest;
+import org.eclipse.aether.DefaultRepositorySystemSession;
+import org.eclipse.aether.RepositorySystem;
+import org.eclipse.aether.RepositorySystemSession;
+import org.eclipse.aether.artifact.DefaultArtifact;
+import org.eclipse.aether.impl.VersionRangeResolver;
+import org.eclipse.aether.repository.LocalRepository;
+import org.eclipse.aether.resolution.VersionRangeRequest;
+import org.eclipse.aether.resolution.VersionRangeResult;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.apache.maven.repository.internal.AbstractRepositoryTest.newTestRepository;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@PlexusTest
+public class DefaultVersionRangeResolverTest {
+
+ @Inject
+ private RepositorySystem system;
+
+ @Inject
+ private VersionRangeResolver versionRangeResolver;
+
+ private RepositorySystemSession session;
+
+ // Each test gets its own local repository (rather than the module-wide target/local-repo) so that a
+ // previously cached resolution from another test cannot mask this test's outcome.
+ @BeforeEach
+ void setUp(@TempDir Path localRepoDir) {
+ DefaultRepositorySystemSession newSession = MavenRepositorySystemUtils.newSession();
+ LocalRepository localRepo = new LocalRepository(localRepoDir.toFile());
+ newSession.setLocalRepositoryManager(system.newLocalRepositoryManager(newSession, localRepo));
+ session = newSession;
+ }
+
+ @Test
+ void testRangeResolutionWithInvalidTokenInMetadataIsRejected() throws Exception {
+ VersionRangeRequest request = new VersionRangeRequest();
+ request.addRepository(newTestRepository());
+ request.setArtifact(new DefaultArtifact("org.apache.maven.its", "dep-invalid-range", "jar", "[1.0,2.0]"));
+
+ VersionRangeResult result = versionRangeResolver.resolveVersionRange(session, request);
+
+ // The metadata carries a versions[] entry that is not a valid coordinate component, so the whole
+ // document is treated as invalid and none of its versions (including the otherwise-valid 1.0 and 2.0)
+ // are offered as candidates for the range.
+ assertTrue(result.getVersions().isEmpty(), "expected no versions, got " + result.getVersions());
+ }
+}
diff --git a/maven-resolver-provider/src/test/java/org/apache/maven/repository/internal/DefaultVersionResolverTest.java b/maven-resolver-provider/src/test/java/org/apache/maven/repository/internal/DefaultVersionResolverTest.java
index 5ad5727913cd..7670b5ea18b0 100644
--- a/maven-resolver-provider/src/test/java/org/apache/maven/repository/internal/DefaultVersionResolverTest.java
+++ b/maven-resolver-provider/src/test/java/org/apache/maven/repository/internal/DefaultVersionResolverTest.java
@@ -78,4 +78,31 @@ public void testResolveSeparateInstalledClassifiedNonVersionedArtifacts() throws
VersionResult resultB = versionResolver.resolveVersion(session, requestB);
assertEquals(versionB, resultB.getVersion());
}
+
+ @Test
+ public void testSnapshotVersionFromMetadataWithInvalidTokenIsRejected() throws Exception {
+ VersionRequest request = new VersionRequest();
+ request.addRepository(newTestRepository());
+ Artifact artifact = new DefaultArtifact("org.apache.maven.its", "dep-invalid-sv", "", "jar", "1.0-SNAPSHOT");
+ request.setArtifact(artifact);
+
+ VersionResult result = versionResolver.resolveVersion(session, request);
+
+ // The metadata carries a snapshotVersion value that is not a valid coordinate component, so the
+ // metadata is treated as invalid and resolution falls back to the requested base version.
+ assertEquals("1.0-SNAPSHOT", result.getVersion());
+ }
+
+ @Test
+ public void testSnapshotTimestampFromMetadataWithInvalidTokenIsRejected() throws Exception {
+ VersionRequest request = new VersionRequest();
+ request.addRepository(newTestRepository());
+ Artifact artifact = new DefaultArtifact("org.apache.maven.its", "dep-invalid-ts", "", "jar", "1.0-SNAPSHOT");
+ request.setArtifact(artifact);
+
+ VersionResult result = versionResolver.resolveVersion(session, request);
+
+ // The metadata carries a snapshot timestamp that is not a valid coordinate component, so the
+ assertEquals("1.0-SNAPSHOT", result.getVersion());
+ }
}
diff --git a/maven-resolver-provider/src/test/java/org/apache/maven/repository/internal/relocation/DistributionManagementArtifactRelocationSourceTest.java b/maven-resolver-provider/src/test/java/org/apache/maven/repository/internal/relocation/DistributionManagementArtifactRelocationSourceTest.java
new file mode 100644
index 000000000000..f0d6b18d454c
--- /dev/null
+++ b/maven-resolver-provider/src/test/java/org/apache/maven/repository/internal/relocation/DistributionManagementArtifactRelocationSourceTest.java
@@ -0,0 +1,109 @@
+/*
+ * 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
+ *
+ * http://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.maven.repository.internal.relocation;
+
+import org.apache.maven.model.DistributionManagement;
+import org.apache.maven.model.Model;
+import org.apache.maven.model.Relocation;
+import org.eclipse.aether.artifact.Artifact;
+import org.eclipse.aether.artifact.DefaultArtifact;
+import org.eclipse.aether.resolution.ArtifactDescriptorException;
+import org.eclipse.aether.resolution.ArtifactDescriptorRequest;
+import org.eclipse.aether.resolution.ArtifactDescriptorResult;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/**
+ * Test cases for {@code DistributionManagementArtifactRelocationSource} coordinate handling.
+ */
+class DistributionManagementArtifactRelocationSourceTest {
+
+ private final DistributionManagementArtifactRelocationSource source =
+ new DistributionManagementArtifactRelocationSource();
+
+ private static ArtifactDescriptorResult newResult() {
+ final ArtifactDescriptorRequest request = new ArtifactDescriptorRequest();
+ request.setArtifact(new DefaultArtifact("ut.simple:artifact:1.0"));
+ return new ArtifactDescriptorResult(request);
+ }
+
+ private static Model newModel(String groupId, String artifactId, String version) {
+ final Relocation relocation = new Relocation();
+ relocation.setGroupId(groupId);
+ relocation.setArtifactId(artifactId);
+ relocation.setVersion(version);
+
+ final DistributionManagement distMgmt = new DistributionManagement();
+ distMgmt.setRelocation(relocation);
+
+ final Model model = new Model();
+ model.setDistributionManagement(distMgmt);
+ return model;
+ }
+
+ @Test
+ void testWellFormedRelocationIsApplied() throws Exception {
+ final Artifact relocated = source.relocatedTarget(null, newResult(), newModel("ut.moved", "artifact", "2.0"));
+
+ assertNotNull(relocated);
+ assertEquals("ut.moved", relocated.getGroupId());
+ assertEquals("artifact", relocated.getArtifactId());
+ assertEquals("2.0", relocated.getVersion());
+ }
+
+ @Test
+ void testRelocationGroupIdWithBackslashIsRejected() {
+ assertThrows(
+ ArtifactDescriptorException.class,
+ () -> source.relocatedTarget(null, newResult(), newModel("a\\b", null, null)));
+ }
+
+ @Test
+ void testRelocationWithInvalidArtifactIdIsRejected() {
+ assertThrows(
+ ArtifactDescriptorException.class,
+ () -> source.relocatedTarget(null, newResult(), newModel(null, "a/b", null)));
+ }
+
+ @Test
+ void testRelocationArtifactIdWithControlCharacterIsRejected() {
+ assertThrows(
+ ArtifactDescriptorException.class,
+ () -> source.relocatedTarget(null, newResult(), newModel(null, "a\nb", null)));
+ }
+
+ @Test
+ void testRelocationWithInvalidVersionIsRejected() {
+ assertThrows(
+ ArtifactDescriptorException.class,
+ () -> source.relocatedTarget(null, newResult(), newModel(null, null, "1.0:2.0")));
+ }
+
+ @Test
+ void testVersionWithTrailingDotsIsAccepted() throws Exception {
+ // only the exact ".." token is rejected; "1.." is an unusual but valid version string
+ final Artifact relocated = source.relocatedTarget(null, newResult(), newModel(null, null, "1.."));
+
+ assertNotNull(relocated);
+ assertEquals("1..", relocated.getVersion());
+ }
+}
diff --git a/maven-resolver-provider/src/test/resources/repo/org/apache/maven/its/dep-invalid-range/maven-metadata.xml b/maven-resolver-provider/src/test/resources/repo/org/apache/maven/its/dep-invalid-range/maven-metadata.xml
new file mode 100644
index 000000000000..637dc3c26a42
--- /dev/null
+++ b/maven-resolver-provider/src/test/resources/repo/org/apache/maven/its/dep-invalid-range/maven-metadata.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+ org.apache.maven.its
+ dep-invalid-range
+
+ 2.0
+ 2.0
+
+ 1.0
+ 1.0:2.0
+ 2.0
+
+ 20120809112920
+
+
diff --git a/maven-resolver-provider/src/test/resources/repo/org/apache/maven/its/dep-invalid-sv/1.0-SNAPSHOT/maven-metadata.xml b/maven-resolver-provider/src/test/resources/repo/org/apache/maven/its/dep-invalid-sv/1.0-SNAPSHOT/maven-metadata.xml
new file mode 100644
index 000000000000..bea32d36ca24
--- /dev/null
+++ b/maven-resolver-provider/src/test/resources/repo/org/apache/maven/its/dep-invalid-sv/1.0-SNAPSHOT/maven-metadata.xml
@@ -0,0 +1,42 @@
+
+
+
+
+
+ org.apache.maven.its
+ dep-invalid-sv
+ 1.0-SNAPSHOT
+
+
+ 20120809.112920
+ 1
+
+ 20120809112920
+
+
+ jar
+ 1.0:2.0
+ 20120809112920
+
+
+
+
diff --git a/maven-resolver-provider/src/test/resources/repo/org/apache/maven/its/dep-invalid-ts/1.0-SNAPSHOT/maven-metadata.xml b/maven-resolver-provider/src/test/resources/repo/org/apache/maven/its/dep-invalid-ts/1.0-SNAPSHOT/maven-metadata.xml
new file mode 100644
index 000000000000..5c096f4886b7
--- /dev/null
+++ b/maven-resolver-provider/src/test/resources/repo/org/apache/maven/its/dep-invalid-ts/1.0-SNAPSHOT/maven-metadata.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+ org.apache.maven.its
+ dep-invalid-ts
+ 1.0-SNAPSHOT
+
+
+ 20120809.112920:1
+ 1
+
+ 20120809112920
+
+