From 7ae5cb4c11913953462de36aaad4903d3738dbc1 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Mon, 13 Jul 2026 15:42:55 +0000 Subject: [PATCH] Validate path components in DefaultLocalPathComposer Reject coordinate components (groupId, artifactId, version, etc.) that contain path traversal sequences ('..') or path separator characters ('/' or '\') before composing the local repository path. This is consistent with the existing path validation in FileTransporter.getPath(). Co-Authored-By: Claude Opus 4.6 --- .../impl/DefaultLocalPathComposer.java | 25 ++++++ .../impl/DefaultLocalPathComposerTest.java | 82 +++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultLocalPathComposerTest.java diff --git a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultLocalPathComposer.java b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultLocalPathComposer.java index e85a4c56bc..9040e45a16 100644 --- a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultLocalPathComposer.java +++ b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/DefaultLocalPathComposer.java @@ -38,6 +38,13 @@ public final class DefaultLocalPathComposer implements LocalPathComposer { public String getPathForArtifact(Artifact artifact, boolean local) { requireNonNull(artifact); + validatePathComponent(artifact.getGroupId(), "groupId"); + validatePathComponent(artifact.getArtifactId(), "artifactId"); + validatePathComponent(artifact.getBaseVersion(), "baseVersion"); + validatePathComponent(artifact.getVersion(), "version"); + validatePathComponent(artifact.getClassifier(), "classifier"); + validatePathComponent(artifact.getExtension(), "extension"); + StringBuilder path = new StringBuilder(128); path.append(artifact.getGroupId().replace('.', '/')).append('/'); @@ -69,6 +76,10 @@ public String getPathForMetadata(Metadata metadata, String repositoryKey) { requireNonNull(metadata); requireNonNull(repositoryKey); + validatePathComponent(metadata.getGroupId(), "groupId"); + validatePathComponent(metadata.getArtifactId(), "artifactId"); + validatePathComponent(metadata.getVersion(), "version"); + StringBuilder path = new StringBuilder(128); if (!metadata.getGroupId().isEmpty()) { @@ -88,6 +99,20 @@ public String getPathForMetadata(Metadata metadata, String repositoryKey) { return path.toString(); } + /** + * Validates that a coordinate component does not contain path traversal sequences + * or path separator characters that could cause the composed path to escape + * the local repository directory. + */ + private static void validatePathComponent(String value, String label) { + if (value != null && !value.isEmpty()) { + if (value.contains("..") || value.contains("/") || value.contains("\\")) { + throw new IllegalArgumentException( + "Invalid " + label + ": must not contain '..', '/' or '\\': " + value); + } + } + } + private String insertRepositoryKey(String metadataType, String repositoryKey) { if (metadataType.contains("/") && !metadataType.endsWith("/")) { int lastSlash = metadataType.lastIndexOf('/'); diff --git a/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultLocalPathComposerTest.java b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultLocalPathComposerTest.java new file mode 100644 index 0000000000..51e442ef99 --- /dev/null +++ b/maven-resolver-impl/src/test/java/org/eclipse/aether/internal/impl/DefaultLocalPathComposerTest.java @@ -0,0 +1,82 @@ +/* + * 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.eclipse.aether.internal.impl; + +import org.eclipse.aether.artifact.DefaultArtifact; +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 DefaultLocalPathComposerTest { + + private final DefaultLocalPathComposer composer = new DefaultLocalPathComposer(); + + @Test + void testGetPathForArtifact() { + String path = composer.getPathForArtifact(new DefaultArtifact("g.id", "a-id", "jar", "1.0"), true); + assertEquals("g/id/a-id/1.0/a-id-1.0.jar", path); + } + + @Test + void testGetPathForMetadata() { + Metadata metadata = new DefaultMetadata("g.id", "a-id", "1.0", "maven-metadata.xml", Metadata.Nature.RELEASE); + String path = composer.getPathForMetadata(metadata, "central"); + assertEquals("g/id/a-id/1.0/maven-metadata-central.xml", path); + } + + @Test + void testGetPathForMetadataRejectsTraversalInVersion() { + Metadata metadata = new DefaultMetadata( + "g.id", "a-id", "../../../../tmp/PWNED-SNAPSHOT", "maven-metadata.xml", Metadata.Nature.RELEASE); + assertThrows(IllegalArgumentException.class, () -> composer.getPathForMetadata(metadata, "central")); + } + + @Test + void testGetPathForArtifactRejectsTraversalInVersion() { + assertThrows( + IllegalArgumentException.class, + () -> composer.getPathForArtifact(new DefaultArtifact("g.id", "a-id", "jar", "../../escape"), true)); + } + + @Test + void testGetPathForMetadataRejectsSlashInVersion() { + Metadata metadata = + new DefaultMetadata("g.id", "a-id", "1.0/../../etc", "maven-metadata.xml", Metadata.Nature.RELEASE); + assertThrows(IllegalArgumentException.class, () -> composer.getPathForMetadata(metadata, "central")); + } + + @Test + void testGetPathForArtifactRejectsBackslashInVersion() { + assertThrows( + IllegalArgumentException.class, + () -> composer.getPathForArtifact(new DefaultArtifact("g.id", "a-id", "jar", "1.0\\..\\escape"), true)); + } + + @Test + void testGetPathForMetadataAcceptsNormalVersions() { + // Verify that normal version strings with dots and dashes are still accepted + Metadata metadata = + new DefaultMetadata("g.id", "a-id", "1.0.0-SNAPSHOT", "maven-metadata.xml", Metadata.Nature.SNAPSHOT); + String path = composer.getPathForMetadata(metadata, "central"); + assertEquals("g/id/a-id/1.0.0-SNAPSHOT/maven-metadata-central.xml", path); + } +}