Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ public String getRemoteFilename() {

@Override
public String getLocalFilename(ArtifactRepository repository) {
return insertRepositoryKey(getRemoteFilename(), repository.getKey());
return insertRepositoryKey(getRemoteFilename(), validateRepositoryKey(repository.getKey()));
}

private String insertRepositoryKey(String filename, String repositoryKey) {
Expand All @@ -242,6 +242,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;
}

@Override
public void merge(org.apache.maven.repository.legacy.metadata.ArtifactMetadata metadata) {
// not used
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,33 @@ public String getRemoteFilename() {

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

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
import org.apache.maven.artifact.repository.RepositoryRequest;
import org.apache.maven.metadata.v4.MetadataStaxReader;
import org.apache.maven.metadata.v4.MetadataStaxWriter;
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;
Expand Down Expand Up @@ -118,6 +119,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());

Expand All @@ -133,12 +145,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);
}
}
Expand Down Expand Up @@ -273,7 +285,11 @@ private boolean loadMetadata(
protected Metadata readMetadata(File mappingFile) throws RepositoryMetadataReadException {

try (InputStream in = Files.newInputStream(mappingFile.toPath())) {
return new Metadata(new MetadataStaxReader().read(in, false));
Metadata result = new Metadata(new MetadataStaxReader().read(in, false));

validateVersioning(result);

return result;
} catch (FileNotFoundException e) {
throw new RepositoryMetadataReadException("Cannot read metadata from '" + mappingFile + "'", e);
} catch (IOException | XMLStreamException e) {
Expand All @@ -282,6 +298,51 @@ protected Metadata readMetadata(File mappingFile) throws RepositoryMetadataReadE
}
}

/**
* 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.
Expand Down Expand Up @@ -355,7 +416,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()
Expand All @@ -381,6 +442,22 @@ private File getArtifactMetadataFromDeploymentRepository(
return file;
}

/**
* Determines the effective checksum policy for a transfer from the given repository. The
* operator-configured policy (e.g. {@code fail} via {@code -C}/{@code --strict-checksums} or a
* per-repository {@code checksumPolicy}) must govern every remote transfer, so it must not be
* hardcoded at the call sites.
*/
private String getChecksumPolicy(ArtifactMetadata metadata, ArtifactRepository repository) {
if (metadata instanceof RepositoryMetadata repositoryMetadata) {
ArtifactRepositoryPolicy policy = repositoryMetadata.getPolicy(repository);
if (policy != null && policy.getChecksumPolicy() != null) {
return policy.getChecksumPolicy();
}
}
return ArtifactRepositoryPolicy.CHECKSUM_POLICY_WARN;
}

@Override
public void deploy(
ArtifactMetadata metadata, ArtifactRepository localRepository, ArtifactRepository deploymentRepository)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -660,7 +660,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);
Expand All @@ -669,6 +669,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;
}
}

@Override
public void publish(
ArtifactRepository repository, File source, String remotePath, ArtifactTransferListener transferListener)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* 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.apache.maven.artifact.repository.layout.DefaultRepositoryLayout;
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;

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) {
return new DefaultArtifactRepository(id, "http://example.invalid/repo", new DefaultRepositoryLayout());
}

@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("..")));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* 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.repository.ArtifactRepository;
import org.apache.maven.artifact.repository.DefaultArtifactRepository;
import org.apache.maven.artifact.repository.layout.DefaultRepositoryLayout;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

/**
* Ensures that a repository key which may originate from a downloaded POM's {@code <repositories>} section
* cannot select a local metadata file name outside the intended one.
*/
class AbstractRepositoryMetadataTest {

private static ArtifactRepository repository(String id) {
return new DefaultArtifactRepository(id, "http://repo.example/r", new DefaultRepositoryLayout());
}

@Test
void repositoryKeyWithColonIsRejected() {
RepositoryMetadata metadata = new GroupRepositoryMetadata("org.test");
ArtifactRepository repo = repository("central:1.0");

assertThrows(IllegalArgumentException.class, () -> metadata.getLocalFilename(repo));
}

@Test
void repositoryKeyWithDotDotSegmentIsRejected() {
RepositoryMetadata metadata = new GroupRepositoryMetadata("org.test");
ArtifactRepository repo = repository("x/../../../../../../settings");

assertThrows(IllegalArgumentException.class, () -> metadata.getLocalFilename(repo));
}

@Test
void repositoryKeyWithBackslashIsRejected() {
RepositoryMetadata metadata = new GroupRepositoryMetadata("org.test");
ArtifactRepository repo = repository("x\\..\\..\\settings");

assertThrows(IllegalArgumentException.class, () -> metadata.getLocalFilename(repo));
}

@Test
void wellFormedRepositoryKeyProducesExpectedFilename() {
RepositoryMetadata metadata = new GroupRepositoryMetadata("org.test");
ArtifactRepository repo = repository("central");

assertEquals("maven-metadata-central.xml", metadata.getLocalFilename(repo));
}
}
Loading
Loading