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 12:01:04 UTC
(maven-deploy-plugin) 05/06: fix: f011 (LOW) — Opt-in containment knob, per the triage recommendation (pipeline variabl
This is an automated email from the ASF dual-hosted git repository.
gnodet pushed a commit to branch security/audit-low-robustness
in repository https://gitbox.apache.org/repos/asf/maven-deploy-plugin.git
commit 5b6da352a4711986d466d15acccdb7ad10305188
Author: Guillaume Nodet <gn...@gmail.com>
AuthorDate: Mon Aug 31 13:57:52 2026 +0200
fix: f011 (LOW) — Opt-in containment knob, per the triage recommendation (pipeline variabl
Co-Authored-By: Claude Opus 4.6 <no...@anthropic.com>
---
.../maven/plugins/deploy/DeployFileMojo.java | 54 ++++++++++++++++++++++
.../plugins/deploy/DeployFileMojoUnitTest.java | 13 ++++++
2 files changed, 67 insertions(+)
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 f9aa67a..aa67ddb 100644
--- a/src/main/java/org/apache/maven/plugins/deploy/DeployFileMojo.java
+++ b/src/main/java/org/apache/maven/plugins/deploy/DeployFileMojo.java
@@ -194,6 +194,21 @@ public class DeployFileMojo extends AbstractDeployMojo {
@Parameter(property = "maven.deploy.file.skip", defaultValue = "false")
private String skip = Boolean.FALSE.toString();
+ /**
+ * Optional containment directory for all artifact-content paths ({@code file}, {@code files},
+ * {@code sources}, {@code javadoc}, {@code pomFile}): when set, each of those paths must resolve
+ * inside this directory (symlinks are resolved before the comparison) or the build fails.
+ * <p>
+ * Intended for CI/automation that fills deploy-file parameters from pipeline variables: without
+ * containment, any file readable by the build (for example {@code ~/.m2/settings.xml} passed as a
+ * side artifact) can be published to the deployment URL in one invocation. Disabled (no
+ * containment) by default; point it at the project or workspace directory in automated pipelines.
+ *
+ * @since 4.0.0
+ */
+ @Parameter(property = "maven.deploy.file.containedIn")
+ private Path containedIn;
+
void initProperties() throws MojoException {
Path deployedPom;
if (pomFile != null) {
@@ -259,6 +274,13 @@ public class DeployFileMojo extends AbstractDeployMojo {
throw new MojoException(message);
}
+ // containment applies to the operator-supplied paths, before any of them is read;
+ // the temporary POM later extracted from the (already contained) jar is exempt
+ checkContained(file, "file");
+ checkContained(pomFile, "pomFile");
+ checkContained(sources, "sources");
+ checkContained(javadoc, "javadoc");
+
initProperties();
validateCredentialBinding(repositoryId, url.replace(File.separator, "/"));
@@ -392,6 +414,7 @@ public class DeployFileMojo extends AbstractDeployMojo {
file = Paths.get(files.substring(fi, nfi));
}
if (Files.isRegularFile(file)) {
+ checkContained(file, "files");
String extension = getExtension(file);
String type = types.substring(ti, nti).trim();
String classifierEntry = classifiers.substring(ci, nci).trim();
@@ -459,6 +482,37 @@ public class DeployFileMojo extends AbstractDeployMojo {
}
}
+ /**
+ * Enforces the optional {@link #containedIn} containment directory for an artifact-content path.
+ */
+ private void checkContained(Path path, String parameterName) throws MojoException {
+ if (containedIn == null || path == null) {
+ return;
+ }
+ if (!isContainedIn(path, containedIn)) {
+ throw new MojoException("Parameter '" + parameterName + "' resolves to "
+ + path.toAbsolutePath().normalize() + ", which is outside the containment directory "
+ + containedIn.toAbsolutePath().normalize() + " configured with maven.deploy.file.containedIn");
+ }
+ }
+
+ /**
+ * Returns {@code true} when {@code path} resolves inside {@code root}, resolving symlinks where
+ * the paths exist so a link pointing outside the containment directory does not pass.
+ */
+ static boolean isContainedIn(Path path, Path root) {
+ return realOrNormalized(path).startsWith(realOrNormalized(root));
+ }
+
+ private static Path realOrNormalized(Path path) {
+ Path absolute = path.toAbsolutePath().normalize();
+ try {
+ return absolute.toRealPath();
+ } catch (IOException e) {
+ return absolute;
+ }
+ }
+
/**
* Gets the path of the specified artifact within the local repository. Note that the returned path need not exist
* (yet).
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 6fa96a7..3c95dd1 100644
--- a/src/test/java/org/apache/maven/plugins/deploy/DeployFileMojoUnitTest.java
+++ b/src/test/java/org/apache/maven/plugins/deploy/DeployFileMojoUnitTest.java
@@ -18,6 +18,8 @@
*/
package org.apache.maven.plugins.deploy;
+import java.io.IOException;
+import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@@ -168,6 +170,17 @@ class DeployFileMojoUnitTest {
assertEquals("file:///tmp/repo", AbstractDeployMojo.redactUrlUserInfo("file:///tmp/repo"));
}
+ @Test
+ void containmentDirectoryIsEnforced() throws IOException {
+ Path root = Files.createTempDirectory("deploy-file-containment");
+ Path inside = Files.createFile(root.resolve("artifact.jar"));
+
+ assertTrue(DeployFileMojo.isContainedIn(inside, root));
+ assertTrue(DeployFileMojo.isContainedIn(root.resolve("sub/other.jar"), root));
+ assertFalse(DeployFileMojo.isContainedIn(root.resolve("../escaped.jar"), root));
+ assertFalse(DeployFileMojo.isContainedIn(Paths.get("/etc/passwd"), root));
+ }
+
private void setMojoModel(
MockDeployFileMojo mojo, String group, String artifact, String version, String packaging, Parent parent) {
mojo.model = Model.newBuilder()