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()