You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by gn...@apache.org on 2026/08/31 11:57:09 UTC
(maven-deploy-plugin) 03/07: fix: f003 (MEDIUM) — Both legs of the root cause closed with shared validators hoisted to Abs
This is an automated email from the ASF dual-hosted git repository.
gnodet pushed a commit to branch security/audit-medium-fixes
in repository https://gitbox.apache.org/repos/asf/maven-deploy-plugin.git
commit 0e15f77fb479bbedd814b1539f83e656e43d1d5f
Author: Guillaume Nodet <gn...@gmail.com>
AuthorDate: Mon Aug 31 13:46:26 2026 +0200
fix: f003 (MEDIUM) — Both legs of the root cause closed with shared validators hoisted to Abs
Security audit finding f003 (MEDIUM).
Both legs of the root cause closed with shared validators hoisted to AbstractDeployMojo: isValidId now rejects any empty dot-separated segment (kills '.', '..', '.a', 'a.', 'a..b' while keeping the [a-zA-Z0-9._-] allowlist); isValidVersion keeps the separator blocklist and additionally rejects empty, whitespace/ISO-control characters, and all-dots values; new isValidClassifier (same allowlist, not all-dots, absent/empty allowed) and isValidTypeOrExtension (non-empty variant) cover the [...]
Co-Authored-By: Claude Opus 4.6 <no...@anthropic.com>
---
.../maven/plugins/deploy/AbstractDeployMojo.java | 91 ++++++++++++++++++++++
.../maven/plugins/deploy/DeployFileMojo.java | 57 +++++---------
.../apache/maven/plugins/deploy/DeployMojo.java | 11 +++
.../plugins/deploy/DeployFileMojoUnitTest.java | 48 ++++++++++++
4 files changed, 169 insertions(+), 38 deletions(-)
diff --git a/src/main/java/org/apache/maven/plugins/deploy/AbstractDeployMojo.java b/src/main/java/org/apache/maven/plugins/deploy/AbstractDeployMojo.java
index 7ab8339..e9039cb 100644
--- a/src/main/java/org/apache/maven/plugins/deploy/AbstractDeployMojo.java
+++ b/src/main/java/org/apache/maven/plugins/deploy/AbstractDeployMojo.java
@@ -180,6 +180,97 @@ public abstract class AbstractDeployMojo implements Mojo {
|| "0:0:0:0:0:0:0:1".equals(host);
}
+ static final String ILLEGAL_VERSION_CHARS = "\\/:\"<>|?*[](){},";
+
+ /**
+ * Returns {@code true} if passed in string is "valid Maven ID" (groupId or artifactId): only
+ * {@code [a-zA-Z0-9._-]} characters, and no empty dot-separated segment (which rejects values
+ * that are entirely dots such as {@code ".."} - a whole repository-layout path segment - as
+ * well as leading/trailing/consecutive dots).
+ */
+ static boolean isValidId(String id) {
+ if (id == null || id.isEmpty()) {
+ return false;
+ }
+ for (int i = 0; i < id.length(); i++) {
+ char c = id.charAt(i);
+ if (!(c >= 'a' && c <= 'z'
+ || c >= 'A' && c <= 'Z'
+ || c >= '0' && c <= '9'
+ || c == '-'
+ || c == '_'
+ || c == '.')) {
+ return false;
+ }
+ }
+ for (String segment : id.split("\\.", -1)) {
+ if (segment.isEmpty()) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Returns {@code true} if passed in string is "valid Maven (simple, non range, expression, etc.)
+ * version": no path/separator-dangerous characters, no whitespace or control characters, and not
+ * composed entirely of dots (a version is used verbatim as a whole repository-layout path
+ * segment, so {@code ".."} must not pass).
+ */
+ static boolean isValidVersion(String version) {
+ if (version == null || version.isEmpty()) {
+ return false;
+ }
+ boolean seenNonDot = false;
+ for (int i = version.length() - 1; i >= 0; i--) {
+ char c = version.charAt(i);
+ if (ILLEGAL_VERSION_CHARS.indexOf(c) >= 0 || Character.isWhitespace(c) || Character.isISOControl(c)) {
+ return false;
+ }
+ if (c != '.') {
+ seenNonDot = true;
+ }
+ }
+ return seenNonDot;
+ }
+
+ /**
+ * Returns {@code true} if the passed classifier is absent, empty, or layout-safe: the classifier
+ * is embedded verbatim in the repository-layout file name
+ * ({@code artifactId-version-classifier.extension}), so it must use the same character allowlist
+ * as ids and must not be composed entirely of dots.
+ */
+ static boolean isValidClassifier(String classifier) {
+ if (classifier == null || classifier.isEmpty()) {
+ return true;
+ }
+ boolean seenNonDot = false;
+ for (int i = 0; i < classifier.length(); i++) {
+ char c = classifier.charAt(i);
+ if (!(c >= 'a' && c <= 'z'
+ || c >= 'A' && c <= 'Z'
+ || c >= '0' && c <= '9'
+ || c == '-'
+ || c == '_'
+ || c == '.')) {
+ return false;
+ }
+ if (c != '.') {
+ seenNonDot = true;
+ }
+ }
+ return seenNonDot;
+ }
+
+ /**
+ * Returns {@code true} if the passed artifact type / extension / packaging is layout-safe:
+ * non-empty, id character allowlist, not composed entirely of dots (the extension is appended
+ * to the repository-layout file name).
+ */
+ static boolean isValidTypeOrExtension(String type) {
+ return type != null && !type.isEmpty() && isValidClassifier(type);
+ }
+
/**
* Guards the repository id→URL credential binding: Maven resolves the credentials for a
* deployment repository purely by matching its id against a {@code <server>} entry in
diff --git a/src/main/java/org/apache/maven/plugins/deploy/DeployFileMojo.java b/src/main/java/org/apache/maven/plugins/deploy/DeployFileMojo.java
index 82e281c..0ea359b 100644
--- a/src/main/java/org/apache/maven/plugins/deploy/DeployFileMojo.java
+++ b/src/main/java/org/apache/maven/plugins/deploy/DeployFileMojo.java
@@ -57,7 +57,6 @@ import org.apache.maven.api.services.xml.XmlReaderException;
@SuppressWarnings("unused")
public class DeployFileMojo extends AbstractDeployMojo {
private static final String TAR = "tar.";
- private static final String ILLEGAL_VERSION_CHARS = "\\/:\"<>|?*[](){},";
/**
* GroupId of the artifact to be deployed. Retrieved from POM file if specified.
@@ -284,6 +283,14 @@ public class DeployFileMojo extends AbstractDeployMojo {
throw new MojoException("The artifact information is not valid: uses invalid characters.");
}
+ if (!isValidTypeOrExtension(packaging)) {
+ throw new MojoException("The packaging is not valid: uses invalid characters.");
+ }
+
+ if (!isValidClassifier(classifier)) {
+ throw new MojoException("The classifier is not valid: uses invalid characters.");
+ }
+
failIfOffline();
warnIfAffectedPackagingAndMaven(packaging);
@@ -375,12 +382,22 @@ public class DeployFileMojo extends AbstractDeployMojo {
if (Files.isRegularFile(file)) {
String extension = getExtension(file);
String type = types.substring(ti, nti).trim();
+ String classifierEntry = classifiers.substring(ci, nci).trim();
+
+ if (!isValidTypeOrExtension(type)) {
+ throw new MojoException("The 'types' entry '" + type + "' is not valid:"
+ + " uses invalid characters or is empty.");
+ }
+ if (classifierEntry.isEmpty() || !isValidClassifier(classifierEntry)) {
+ throw new MojoException("The 'classifiers' entry '" + classifierEntry + "' is not valid:"
+ + " uses invalid characters or is empty.");
+ }
ProducedArtifact deployable = session.createProducedArtifact(
artifact.getGroupId(),
artifact.getArtifactId(),
artifact.getVersion().toString(),
- classifiers.substring(ci, nci).trim(),
+ classifierEntry,
extension,
type);
artifactManager.setPath(deployable, file);
@@ -592,40 +609,4 @@ public class DeployFileMojo extends AbstractDeployMojo {
}
return "";
}
-
- /**
- * Returns {@code true} if passed in string is "valid Maven ID" (groupId or artifactId).
- */
- private boolean isValidId(String id) {
- if (id == null) {
- return false;
- }
- for (int i = 0; i < id.length(); i++) {
- char c = id.charAt(i);
- if (!(c >= 'a' && c <= 'z'
- || c >= 'A' && c <= 'Z'
- || c >= '0' && c <= '9'
- || c == '-'
- || c == '_'
- || c == '.')) {
- return false;
- }
- }
- return true;
- }
-
- /**
- * Returns {@code true} if passed in string is "valid Maven (simple. non range, expression, etc) version".
- */
- private boolean isValidVersion(String version) {
- if (version == null) {
- return false;
- }
- for (int i = version.length() - 1; i >= 0; i--) {
- if (ILLEGAL_VERSION_CHARS.indexOf(version.charAt(i)) >= 0) {
- return false;
- }
- }
- return true;
- }
}
diff --git a/src/main/java/org/apache/maven/plugins/deploy/DeployMojo.java b/src/main/java/org/apache/maven/plugins/deploy/DeployMojo.java
index 7758893..1df99d0 100644
--- a/src/main/java/org/apache/maven/plugins/deploy/DeployMojo.java
+++ b/src/main/java/org/apache/maven/plugins/deploy/DeployMojo.java
@@ -299,7 +299,18 @@ public class DeployMojo extends AbstractDeployMojo {
artifactManager.setPath(project.getPomArtifact(), project.getPomPath());
}
+ if (!isValidId(project.getGroupId())
+ || !isValidId(project.getArtifactId())
+ || !isValidVersion(project.getVersion())) {
+ throw new MojoException("The project coordinates " + project.getGroupId() + ":" + project.getArtifactId()
+ + ":" + project.getVersion() + " are not valid: they use invalid characters.");
+ }
+
for (Artifact deployable : deployables) {
+ if (!isValidClassifier(deployable.getClassifier())) {
+ throw new MojoException("The classifier of attached artifact " + deployable
+ + " is not valid: uses invalid characters.");
+ }
if (!isValidPath(deployable)) {
if (deployable == project.getMainArtifact().orElse(null)) {
if (attachedArtifacts.isEmpty()) {
diff --git a/src/test/java/org/apache/maven/plugins/deploy/DeployFileMojoUnitTest.java b/src/test/java/org/apache/maven/plugins/deploy/DeployFileMojoUnitTest.java
index 6a96c4e..21f4175 100644
--- a/src/test/java/org/apache/maven/plugins/deploy/DeployFileMojoUnitTest.java
+++ b/src/test/java/org/apache/maven/plugins/deploy/DeployFileMojoUnitTest.java
@@ -28,6 +28,8 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* @author <a href="jerome@coffeebreaks.org">Jerome Lacoste</a>
@@ -107,6 +109,52 @@ class DeployFileMojoUnitTest {
assertEquals(expectedPackaging, mojo.getPackaging());
}
+ @Test
+ void idValidationRejectsDotOnlyAndEmptySegments() {
+ assertFalse(AbstractDeployMojo.isValidId("."));
+ assertFalse(AbstractDeployMojo.isValidId(".."));
+ assertFalse(AbstractDeployMojo.isValidId(".a"));
+ assertFalse(AbstractDeployMojo.isValidId("a."));
+ assertFalse(AbstractDeployMojo.isValidId("a..b"));
+ assertFalse(AbstractDeployMojo.isValidId("a/b"));
+ assertFalse(AbstractDeployMojo.isValidId("a\\b"));
+ assertFalse(AbstractDeployMojo.isValidId(""));
+ assertFalse(AbstractDeployMojo.isValidId(null));
+ assertTrue(AbstractDeployMojo.isValidId("org.apache.maven"));
+ assertTrue(AbstractDeployMojo.isValidId("maven-deploy-plugin"));
+ }
+
+ @Test
+ void versionValidationRejectsDotOnlyWhitespaceAndControlChars() {
+ assertFalse(AbstractDeployMojo.isValidVersion("."));
+ assertFalse(AbstractDeployMojo.isValidVersion(".."));
+ assertFalse(AbstractDeployMojo.isValidVersion("1.0 "));
+ assertFalse(AbstractDeployMojo.isValidVersion("1\t0"));
+ assertFalse(AbstractDeployMojo.isValidVersion("1.0/x"));
+ assertFalse(AbstractDeployMojo.isValidVersion(""));
+ assertFalse(AbstractDeployMojo.isValidVersion(null));
+ assertTrue(AbstractDeployMojo.isValidVersion("1.0-SNAPSHOT"));
+ assertTrue(AbstractDeployMojo.isValidVersion("4.0.0-beta-3"));
+ }
+
+ @Test
+ void classifierAndTypeValidationRejectsLayoutTraversal() {
+ assertFalse(AbstractDeployMojo.isValidClassifier("../../../../org/other/1.0/other-1.0"));
+ assertFalse(AbstractDeployMojo.isValidClassifier(".."));
+ assertFalse(AbstractDeployMojo.isValidClassifier("a b"));
+ assertTrue(AbstractDeployMojo.isValidClassifier(null));
+ assertTrue(AbstractDeployMojo.isValidClassifier(""));
+ assertTrue(AbstractDeployMojo.isValidClassifier("sources"));
+ assertTrue(AbstractDeployMojo.isValidClassifier("site.pdf"));
+ assertFalse(AbstractDeployMojo.isValidTypeOrExtension(null));
+ assertFalse(AbstractDeployMojo.isValidTypeOrExtension(""));
+ assertFalse(AbstractDeployMojo.isValidTypeOrExtension(".."));
+ assertFalse(AbstractDeployMojo.isValidTypeOrExtension("jar/../x"));
+ assertTrue(AbstractDeployMojo.isValidTypeOrExtension("jar"));
+ assertTrue(AbstractDeployMojo.isValidTypeOrExtension("tar.gz"));
+ assertTrue(AbstractDeployMojo.isValidTypeOrExtension("maven-plugin"));
+ }
+
private void setMojoModel(
MockDeployFileMojo mojo, String group, String artifact, String version, String packaging, Parent parent) {
mojo.model = Model.newBuilder()