From da6bed4f244c445de389c8881f64fa67c3a1c567 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Tue, 1 Sep 2026 13:31:31 +0200 Subject: [PATCH] Validate repository key and metadata version tokens, honour checksum policy Backport of the path traversal validation and checksum policy fixes from #12950 (master) / #12945 (maven-4.0.x) / #12978 (maven-3.10.x) to the maven-3.9.x branch. - Path traversal: AbstractRepositoryMetadata.getLocalFilename() and LegacyLocalRepositoryManager.ArtifactMetadataAdapter.getLocalFilename() now reject repository keys containing '..', '/', '\', ':', or ISO control characters. - Metadata token validation: DefaultRepositoryMetadataManager.readMetadata() validates every version token (latest, release, versions, snapshot versions, snapshot timestamp) for the same characters after parsing. - Checksum policy: resolve() now catches ChecksumFailedException separately and fails metadata resolution under checksumPolicy=fail. getArtifactMetadataFromDeploymentRepository() and LegacyRepositorySystem.retrieve() resolve the effective policy from the repository configuration instead of hardcoding 'warn'. The proxy clone fix (DefaultSettingsDecrypter) is not included because 3.9.x already clones each proxy before decryption. Co-Authored-By: Claude Opus 4.6 --- .../DefaultRepositoryMetadataManager.java | 74 ++++++++++++++++- .../legacy/LegacyRepositorySystem.java | 35 +++++++- .../DefaultRepositoryMetadataManagerTest.java | 83 +++++++++++++++++++ ...positoryMetadataManagerValidationTest.java | 63 ++++++++++++++ .../legacy/LegacyRepositorySystemTest.java | 26 ++++++ .../maven-metadata.xml | 31 +++++++ .../metadata-invalid-token/maven-metadata.xml | 28 +++++++ .../LegacyLocalRepositoryManager.java | 28 ++++++- .../metadata/AbstractRepositoryMetadata.java | 28 ++++++- .../LegacyLocalRepositoryManagerTest.java | 61 ++++++++++++++ .../AbstractRepositoryMetadataTest.java | 81 ++++++++++++++++++ 11 files changed, 533 insertions(+), 5 deletions(-) create mode 100644 maven-compat/src/test/java/org/apache/maven/artifact/repository/metadata/DefaultRepositoryMetadataManagerTest.java create mode 100644 maven-compat/src/test/java/org/apache/maven/artifact/repository/metadata/DefaultRepositoryMetadataManagerValidationTest.java create mode 100644 maven-compat/src/test/resources/metadata-invalid-timestamp/maven-metadata.xml create mode 100644 maven-compat/src/test/resources/metadata-invalid-token/maven-metadata.xml create mode 100644 maven-core/src/test/java/org/apache/maven/artifact/repository/LegacyLocalRepositoryManagerTest.java create mode 100644 maven-core/src/test/java/org/apache/maven/artifact/repository/metadata/AbstractRepositoryMetadataTest.java 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 54ca354e2b4b..c3c2c5e07a81 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 @@ -35,6 +35,7 @@ import org.apache.maven.artifact.repository.RepositoryRequest; import org.apache.maven.artifact.repository.metadata.io.xpp3.MetadataXpp3Reader; import org.apache.maven.artifact.repository.metadata.io.xpp3.MetadataXpp3Writer; +import org.apache.maven.repository.legacy.ChecksumFailedException; import org.apache.maven.repository.legacy.UpdateCheckManager; import org.apache.maven.repository.legacy.WagonManager; import org.apache.maven.wagon.ResourceDoesNotExistException; @@ -114,6 +115,17 @@ public void resolve(RepositoryMetadata metadata, RepositoryRequest request) getLogger().info(metadata.getKey() + ": checking for updates from " + repository.getId()); try { wagonManager.getArtifactMetadata(metadata, repository, file, policy.getChecksumPolicy()); + updateCheckManager.touch(metadata, repository, file); + } catch (ChecksumFailedException e) { + // ChecksumFailedException is only thrown by the wagon manager under + // CHECKSUM_POLICY_FAIL: honor the strict policy by failing metadata resolution + // instead of downgrading the integrity failure to a warning. The update + // tracking file is deliberately not touched, so the next build retries + // immediately instead of trusting stale metadata for a full update interval. + throw new RepositoryMetadataResolutionException( + metadata + " failed checksum verification against repository: " + repository.getId() + + " due to an error: " + e.getMessage(), + e); } catch (ResourceDoesNotExistException e) { getLogger().debug(metadata + " could not be found on repository: " + repository.getId()); @@ -129,12 +141,12 @@ public void resolve(RepositoryMetadata metadata, RepositoryRequest request) file.delete(); // if this fails, forget about it } } + updateCheckManager.touch(metadata, repository, file); } catch (TransferFailedException e) { getLogger() .warn(metadata + " could not be retrieved from repository: " + repository.getId() + " due to an error: " + e.getMessage()); getLogger().debug("Exception", e); - } finally { updateCheckManager.touch(metadata, repository, file); } } @@ -279,9 +291,57 @@ protected Metadata readMetadata(File mappingFile) throws RepositoryMetadataReadE throw new RepositoryMetadataReadException( "Cannot read metadata from '" + mappingFile + "': " + e.getMessage(), e); } + + validateVersioning(result); + 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. @@ -354,7 +414,7 @@ private File getArtifactMetadataFromDeploymentRepository( try { wagonManager.getArtifactMetadataFromDeploymentRepository( - metadata, remoteRepository, file, ArtifactRepositoryPolicy.CHECKSUM_POLICY_WARN); + metadata, remoteRepository, file, getChecksumPolicy(metadata, remoteRepository)); } catch (ResourceDoesNotExistException e) { getLogger() .info(metadata + " could not be found on repository: " + remoteRepository.getId() @@ -380,6 +440,16 @@ private File getArtifactMetadataFromDeploymentRepository( return file; } + private String getChecksumPolicy(ArtifactMetadata metadata, ArtifactRepository repository) { + if (metadata instanceof RepositoryMetadata) { + ArtifactRepositoryPolicy policy = ((RepositoryMetadata) metadata).getPolicy(repository); + if (policy != null && policy.getChecksumPolicy() != null) { + return policy.getChecksumPolicy(); + } + } + return ArtifactRepositoryPolicy.CHECKSUM_POLICY_WARN; + } + public void deploy( ArtifactMetadata metadata, ArtifactRepository localRepository, ArtifactRepository deploymentRepository) throws RepositoryMetadataDeploymentException { diff --git a/maven-compat/src/main/java/org/apache/maven/repository/legacy/LegacyRepositorySystem.java b/maven-compat/src/main/java/org/apache/maven/repository/legacy/LegacyRepositorySystem.java index 677c0ac8cfc3..9c2fad1ce0f6 100644 --- a/maven-compat/src/main/java/org/apache/maven/repository/legacy/LegacyRepositorySystem.java +++ b/maven-compat/src/main/java/org/apache/maven/repository/legacy/LegacyRepositorySystem.java @@ -662,7 +662,7 @@ public void retrieve( destination, remotePath, TransferListenerAdapter.newAdapter(transferListener), - ArtifactRepositoryPolicy.CHECKSUM_POLICY_WARN, + getChecksumPolicy(repository), true); } catch (org.apache.maven.wagon.TransferFailedException e) { throw new ArtifactTransferFailedException(getMessage(e, "Error transferring artifact."), e); @@ -671,6 +671,39 @@ public void retrieve( } } + /** + * Determines the effective checksum policy for a generic retrieval from the given repository. + * The operator-configured policy (e.g. {@code fail} via {@code -C}/{@code --strict-checksums}) + * must govern every remote transfer instead of a hardcoded lenient default. A generic remote + * path cannot be classified as release or snapshot, so the stricter of the two configured + * policies applies. + */ + private static String getChecksumPolicy(ArtifactRepository repository) { + String releases = + (repository.getReleases() != null) ? repository.getReleases().getChecksumPolicy() : null; + String snapshots = + (repository.getSnapshots() != null) ? repository.getSnapshots().getChecksumPolicy() : null; + String policy; + if (releases == null) { + policy = snapshots; + } else if (snapshots == null || checksumRank(releases) >= checksumRank(snapshots)) { + policy = releases; + } else { + policy = snapshots; + } + return (policy != null) ? policy : ArtifactRepositoryPolicy.CHECKSUM_POLICY_WARN; + } + + private static int checksumRank(String policy) { + if (ArtifactRepositoryPolicy.CHECKSUM_POLICY_FAIL.equals(policy)) { + return 2; + } else if (ArtifactRepositoryPolicy.CHECKSUM_POLICY_IGNORE.equals(policy)) { + return 0; + } else { + return 1; + } + } + public void publish( ArtifactRepository repository, File source, String remotePath, ArtifactTransferListener transferListener) throws ArtifactTransferFailedException { 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..e4bf982c3274 --- /dev/null +++ b/maven-compat/src/test/java/org/apache/maven/artifact/repository/metadata/DefaultRepositoryMetadataManagerTest.java @@ -0,0 +1,83 @@ +/* + * 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 javax.inject.Inject; +import javax.inject.Named; + +import java.io.File; +import java.util.Collections; + +import org.apache.maven.artifact.AbstractArtifactComponentTest; +import org.apache.maven.artifact.repository.ArtifactRepository; +import org.apache.maven.artifact.repository.ArtifactRepositoryPolicy; +import org.apache.maven.artifact.repository.layout.ArtifactRepositoryLayout; +import org.codehaus.plexus.testing.PlexusTest; +import org.codehaus.plexus.util.FileUtils; +import org.junit.jupiter.api.Test; + +import static org.codehaus.plexus.testing.PlexusExtension.getBasedir; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Tests {@link DefaultRepositoryMetadataManager}. + */ +@PlexusTest +@Deprecated +class DefaultRepositoryMetadataManagerTest extends AbstractArtifactComponentTest { + + @Inject + private RepositoryMetadataManager repositoryMetadataManager; + + @Inject + @Named("default") + private ArtifactRepositoryLayout layout; + + @Override + protected String component() { + return "repositoryMetadataManager"; + } + + @Test + void testResolveHonorsConfiguredFailChecksumPolicy() throws Exception { + RepositoryMetadata metadata = new GroupRepositoryMetadata("checksum-policy-test-group"); + + ArtifactRepositoryPolicy failPolicy = new ArtifactRepositoryPolicy( + true, ArtifactRepositoryPolicy.UPDATE_POLICY_ALWAYS, ArtifactRepositoryPolicy.CHECKSUM_POLICY_FAIL); + + File remoteBase = new File(getBasedir(), "target/test-repositories/" + component() + "/remote-repository"); + FileUtils.deleteDirectory(remoteBase); + + ArtifactRepository remoteRepo = artifactRepositoryFactory.createArtifactRepository( + "test", "file://" + remoteBase.getPath(), layout, failPolicy, failPolicy); + + String remotePath = remoteRepo.pathOfRemoteRepositoryMetadata(metadata); + File remoteFile = new File(remoteBase, remotePath); + remoteFile.getParentFile().mkdirs(); + FileUtils.fileWrite(remoteFile.getAbsolutePath(), ""); + FileUtils.fileWrite(remoteFile.getAbsolutePath() + ".sha1", "0000000000000000000000000000000000000000"); + + ArtifactRepository localRepo = localRepository(); + FileUtils.deleteDirectory(new File(localRepo.getBasedir())); + + assertThrows( + RepositoryMetadataResolutionException.class, + () -> repositoryMetadataManager.resolve(metadata, Collections.singletonList(remoteRepo), localRepo)); + } +} diff --git a/maven-compat/src/test/java/org/apache/maven/artifact/repository/metadata/DefaultRepositoryMetadataManagerValidationTest.java b/maven-compat/src/test/java/org/apache/maven/artifact/repository/metadata/DefaultRepositoryMetadataManagerValidationTest.java new file mode 100644 index 000000000000..2167606b810b --- /dev/null +++ b/maven-compat/src/test/java/org/apache/maven/artifact/repository/metadata/DefaultRepositoryMetadataManagerValidationTest.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. + */ +class DefaultRepositoryMetadataManagerValidationTest { + + 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/java/org/apache/maven/repository/legacy/LegacyRepositorySystemTest.java b/maven-compat/src/test/java/org/apache/maven/repository/legacy/LegacyRepositorySystemTest.java index 9e723531c96f..d931f195ed10 100644 --- a/maven-compat/src/test/java/org/apache/maven/repository/legacy/LegacyRepositorySystemTest.java +++ b/maven-compat/src/test/java/org/apache/maven/repository/legacy/LegacyRepositorySystemTest.java @@ -24,14 +24,19 @@ import java.util.Arrays; import org.apache.maven.artifact.repository.ArtifactRepository; +import org.apache.maven.artifact.repository.ArtifactRepositoryPolicy; import org.apache.maven.artifact.repository.Authentication; +import org.apache.maven.repository.ArtifactTransferFailedException; import org.apache.maven.repository.RepositorySystem; import org.apache.maven.settings.Server; import org.codehaus.plexus.testing.PlexusTest; +import org.codehaus.plexus.util.FileUtils; 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; +import static org.junit.jupiter.api.Assertions.assertThrows; /** * Tests {@link LegacyRepositorySystem}. @@ -66,4 +71,25 @@ public void testAuthenticationHandling() throws Exception { assertEquals("jason", authentication.getUsername()); assertEquals("abc123", authentication.getPassword()); } + + @Test + public void testRetrieveHonorsConfiguredFailChecksumPolicy(@TempDir File tempDir) throws Exception { + File remoteBase = new File(tempDir, "remote"); + remoteBase.mkdirs(); + File remoteFile = new File(remoteBase, "sample.txt"); + FileUtils.fileWrite(remoteFile.getAbsolutePath(), "content"); + FileUtils.fileWrite( + new File(remoteBase, "sample.txt.sha1").getAbsolutePath(), "0000000000000000000000000000000000000000"); + + ArtifactRepositoryPolicy failPolicy = new ArtifactRepositoryPolicy( + true, ArtifactRepositoryPolicy.UPDATE_POLICY_ALWAYS, ArtifactRepositoryPolicy.CHECKSUM_POLICY_FAIL); + ArtifactRepository repository = repositorySystem.createArtifactRepository( + "test", "file://" + remoteBase.getAbsolutePath(), null, failPolicy, failPolicy); + + File destination = new File(tempDir, "sample.txt"); + + assertThrows( + ArtifactTransferFailedException.class, + () -> repositorySystem.retrieve(repository, destination, "sample.txt", null)); + } } 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..68cf46a41f3d --- /dev/null +++ b/maven-compat/src/test/resources/metadata-invalid-timestamp/maven-metadata.xml @@ -0,0 +1,31 @@ + + + + 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..8f4eb7200c7b --- /dev/null +++ b/maven-compat/src/test/resources/metadata-invalid-token/maven-metadata.xml @@ -0,0 +1,28 @@ + + + + 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/LegacyLocalRepositoryManager.java b/maven-core/src/main/java/org/apache/maven/artifact/repository/LegacyLocalRepositoryManager.java index cd76c79d78d0..7a01b8d6482c 100644 --- a/maven-core/src/main/java/org/apache/maven/artifact/repository/LegacyLocalRepositoryManager.java +++ b/maven-core/src/main/java/org/apache/maven/artifact/repository/LegacyLocalRepositoryManager.java @@ -207,7 +207,7 @@ public String getRemoteFilename() { } public String getLocalFilename(ArtifactRepository repository) { - return insertRepositoryKey(getRemoteFilename(), repository.getKey()); + return insertRepositoryKey(getRemoteFilename(), validateRepositoryKey(repository.getKey())); } private String insertRepositoryKey(String filename, String repositoryKey) { @@ -221,6 +221,32 @@ private String insertRepositoryKey(String filename, String repositoryKey) { return result; } + /** + * The repository key (its id) is used verbatim as part of a local file name, so it must lie within + * the usual coordinate character set. + */ + private static String validateRepositoryKey(String key) { + if (key == null || key.isEmpty()) { + return key; + } + if (isInvalidPathToken(key)) { + throw new IllegalArgumentException("Invalid repository key '" + key + "'"); + } + return key; + } + + private static boolean isInvalidPathToken(String value) { + if ("..".equals(value) || value.indexOf('/') >= 0 || value.indexOf('\\') >= 0 || value.indexOf(':') >= 0) { + return true; + } + for (int i = 0; i < value.length(); i++) { + if (Character.isISOControl(value.charAt(i))) { + return true; + } + } + return false; + } + public void merge(org.apache.maven.repository.legacy.metadata.ArtifactMetadata metadata) { // not used } diff --git a/maven-core/src/main/java/org/apache/maven/artifact/repository/metadata/AbstractRepositoryMetadata.java b/maven-core/src/main/java/org/apache/maven/artifact/repository/metadata/AbstractRepositoryMetadata.java index 82bd9fea5a75..a9569972703d 100644 --- a/maven-core/src/main/java/org/apache/maven/artifact/repository/metadata/AbstractRepositoryMetadata.java +++ b/maven-core/src/main/java/org/apache/maven/artifact/repository/metadata/AbstractRepositoryMetadata.java @@ -50,7 +50,33 @@ public String getRemoteFilename() { } public String getLocalFilename(ArtifactRepository repository) { - return "maven-metadata-" + repository.getKey() + ".xml"; + return "maven-metadata-" + validateRepositoryKey(repository.getKey()) + ".xml"; + } + + /** + * The repository key (its id) is used verbatim as part of a local file name, so it must lie within + * the usual coordinate character set. + */ + private static String validateRepositoryKey(String key) { + if (key == null || key.isEmpty()) { + return key; + } + if (isInvalidPathToken(key)) { + throw new IllegalArgumentException("Invalid repository key '" + key + "'"); + } + return key; + } + + private static boolean isInvalidPathToken(String value) { + if ("..".equals(value) || value.indexOf('/') >= 0 || value.indexOf('\\') >= 0 || value.indexOf(':') >= 0) { + return true; + } + for (int i = 0; i < value.length(); i++) { + if (Character.isISOControl(value.charAt(i))) { + return true; + } + } + return false; } public void storeInLocalRepository(ArtifactRepository localRepository, ArtifactRepository remoteRepository) diff --git a/maven-core/src/test/java/org/apache/maven/artifact/repository/LegacyLocalRepositoryManagerTest.java b/maven-core/src/test/java/org/apache/maven/artifact/repository/LegacyLocalRepositoryManagerTest.java new file mode 100644 index 000000000000..a677ea4c224a --- /dev/null +++ b/maven-core/src/test/java/org/apache/maven/artifact/repository/LegacyLocalRepositoryManagerTest.java @@ -0,0 +1,61 @@ +/* + * 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; + +import org.eclipse.aether.metadata.DefaultMetadata; +import org.eclipse.aether.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.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class LegacyLocalRepositoryManagerTest { + + private static LegacyLocalRepositoryManager.ArtifactMetadataAdapter newAdapter() { + Metadata metadata = + new DefaultMetadata("g", "a", "1.0", "maven-metadata.xml", Metadata.Nature.RELEASE_OR_SNAPSHOT); + return new LegacyLocalRepositoryManager.ArtifactMetadataAdapter(metadata); + } + + private static ArtifactRepository repositoryWithId(String id) { + ArtifactRepository repo = mock(ArtifactRepository.class); + when(repo.getKey()).thenReturn(id); + return repo; + } + + @Test + void getLocalFilenameKeepsWellFormedRepositoryKeyUnchanged() { + String filename = newAdapter().getLocalFilename(repositoryWithId("central")); + + assertEquals("maven-metadata-central.xml", filename); + } + + @Test + void getLocalFilenameRejectsRepositoryKeyContainingPathSeparator() { + assertThrows( + IllegalArgumentException.class, () -> newAdapter().getLocalFilename(repositoryWithId("repo/evil"))); + } + + @Test + void getLocalFilenameRejectsRepositoryKeyThatIsAParentDirectoryReference() { + assertThrows(IllegalArgumentException.class, () -> newAdapter().getLocalFilename(repositoryWithId(".."))); + } +} diff --git a/maven-core/src/test/java/org/apache/maven/artifact/repository/metadata/AbstractRepositoryMetadataTest.java b/maven-core/src/test/java/org/apache/maven/artifact/repository/metadata/AbstractRepositoryMetadataTest.java new file mode 100644 index 000000000000..a8d2aeef06bf --- /dev/null +++ b/maven-core/src/test/java/org/apache/maven/artifact/repository/metadata/AbstractRepositoryMetadataTest.java @@ -0,0 +1,81 @@ +/* + * 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 org.apache.maven.artifact.Artifact; +import org.apache.maven.artifact.repository.ArtifactRepository; +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.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Ensures that a repository key which may originate from a downloaded POM's {@code } section + * cannot select a local metadata file name outside the intended one. + */ +class AbstractRepositoryMetadataTest { + + private static ArtifactRepository repository(String id) { + ArtifactRepository repo = mock(ArtifactRepository.class); + when(repo.getKey()).thenReturn(id); + return repo; + } + + private static RepositoryMetadata createMetadata() { + Artifact artifact = mock(Artifact.class); + when(artifact.getGroupId()).thenReturn("org.test"); + when(artifact.getArtifactId()).thenReturn("test-artifact"); + when(artifact.getVersion()).thenReturn("1.0"); + return new ArtifactRepositoryMetadata(artifact); + } + + @Test + void repositoryKeyWithColonIsRejected() { + RepositoryMetadata metadata = createMetadata(); + ArtifactRepository repo = repository("central:1.0"); + + assertThrows(IllegalArgumentException.class, () -> metadata.getLocalFilename(repo)); + } + + @Test + void repositoryKeyWithDotDotSegmentIsRejected() { + RepositoryMetadata metadata = createMetadata(); + ArtifactRepository repo = repository("x/../../../../../../settings"); + + assertThrows(IllegalArgumentException.class, () -> metadata.getLocalFilename(repo)); + } + + @Test + void repositoryKeyWithBackslashIsRejected() { + RepositoryMetadata metadata = createMetadata(); + ArtifactRepository repo = repository("x\\..\\..\\settings"); + + assertThrows(IllegalArgumentException.class, () -> metadata.getLocalFilename(repo)); + } + + @Test + void wellFormedRepositoryKeyProducesExpectedFilename() { + RepositoryMetadata metadata = createMetadata(); + ArtifactRepository repo = repository("central"); + + assertEquals("maven-metadata-central.xml", metadata.getLocalFilename(repo)); + } +}