From 66eaf750d3a2ed0f9f6c23067c585d79e5d70669 Mon Sep 17 00:00:00 2001 From: Sylwester Lachiewicz Date: Sun, 30 Aug 2026 14:36:36 +0200 Subject: [PATCH 1/3] Limit property interpolation in resolved dependency POMs Models built at VALIDATION_LEVEL_MINIMAL are the ones Maven builds while resolving dependency, parent and BOM-import POMs from a repository, not the operator's own project (see loadPom() in DefaultArtifactDescriptorReader and the BOM-import path in DefaultModelBuilder). Such models now interpolate their user/system properties only against a small, environment-independent allowlist (java.*, os.*, maven.*, the separator properties, and the CI-friendly revision/changelist/sha1); everything else, including env.* and arbitrary -D properties, is left as a literal unresolved expression so it cannot be substituted into things like repository URLs. Operator project builds use a higher validation level and are unaffected. The prior behavior can be restored globally with the new maven.model.dependencyInterpolation.full system/user property. --- .../AbstractStringBasedModelInterpolator.java | 72 ++++++++++++++-- .../AbstractModelInterpolatorTest.java | 85 +++++++++++++++++++ 2 files changed, 148 insertions(+), 9 deletions(-) diff --git a/maven-model-builder/src/main/java/org/apache/maven/model/interpolation/AbstractStringBasedModelInterpolator.java b/maven-model-builder/src/main/java/org/apache/maven/model/interpolation/AbstractStringBasedModelInterpolator.java index b953aff45360..58ed19c1371d 100644 --- a/maven-model-builder/src/main/java/org/apache/maven/model/interpolation/AbstractStringBasedModelInterpolator.java +++ b/maven-model-builder/src/main/java/org/apache/maven/model/interpolation/AbstractStringBasedModelInterpolator.java @@ -51,6 +51,15 @@ * @author jdcasey Created on Feb 3, 2005 */ public abstract class AbstractStringBasedModelInterpolator implements ModelInterpolator { + + /** + * The name of the property that opts a build back into the previous behavior of + * interpolating models built at {@link ModelBuildingRequest#VALIDATION_LEVEL_MINIMAL} + * against the full set of session (system, environment and CLI) properties. Disabled + * by default. + */ + public static final String FULL_EXTERNAL_INTERPOLATION_PROPERTY = "maven.model.dependencyInterpolation.full"; + private static final List PROJECT_PREFIXES = Arrays.asList("pom.", "project."); private static final Collection TRANSLATED_PATH_EXPRESSIONS; @@ -180,21 +189,34 @@ public Object getValue(String expression) { valueSources.add(modelValueSource1); - valueSources.add(new MapBasedValueSource(config.getUserProperties())); + // Models built at VALIDATION_LEVEL_MINIMAL are the models Maven builds while resolving + // dependency, parent and BOM-import POMs from a repository, not the operator's own + // project. Such models interpolate only against their own properties and a small set + // of environment-independent expressions; everything else in the user/system property + // space stays uninterpolated. Operator project builds use a higher validation level and + // keep the full set of value sources, unchanged from previous behavior. + boolean restricted = restrictExternalModelInterpolation(config); + + ValueSource userPropertiesValueSource = new MapBasedValueSource(config.getUserProperties()); + valueSources.add(restricted ? restrictToSafeExpressions(userPropertiesValueSource) : userPropertiesValueSource); // Overwrite existing values in model properties. Otherwise it's not possible // to define them via command line e.g.: mvn -Drevision=6.5.7 ... versionProcessor.overwriteModelProperties(modelProperties, config); valueSources.add(new MapBasedValueSource(modelProperties)); - valueSources.add(new MapBasedValueSource(config.getSystemProperties())); - - valueSources.add(new AbstractValueSource(false) { - @Override - public Object getValue(String expression) { - return config.getSystemProperties().getProperty("env." + expression); - } - }); + ValueSource systemPropertiesValueSource = new MapBasedValueSource(config.getSystemProperties()); + valueSources.add( + restricted ? restrictToSafeExpressions(systemPropertiesValueSource) : systemPropertiesValueSource); + + if (!restricted) { + valueSources.add(new AbstractValueSource(false) { + @Override + public Object getValue(String expression) { + return config.getSystemProperties().getProperty("env." + expression); + } + }); + } valueSources.add(modelValueSource2); @@ -204,6 +226,38 @@ public Object getValue(String expression) { return valueSources; } + private static boolean restrictExternalModelInterpolation(ModelBuildingRequest config) { + return config.getValidationLevel() < ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_2_0 + && !Boolean.parseBoolean(config.getSystemProperties().getProperty(FULL_EXTERNAL_INTERPOLATION_PROPERTY)) + && !Boolean.parseBoolean(config.getUserProperties().getProperty(FULL_EXTERNAL_INTERPOLATION_PROPERTY)); + } + + private static ValueSource restrictToSafeExpressions(ValueSource source) { + return new AbstractValueSource(false) { + @Override + public Object getValue(String expression) { + return isSafeExternalExpression(expression) ? source.getValue(expression) : null; + } + }; + } + + /** + * Expressions that models built at {@link ModelBuildingRequest#VALIDATION_LEVEL_MINIMAL} + * may still resolve from the session properties: JVM- and Maven-defined properties, plus + * the CI-friendly version properties (MNG-5895). All other expressions are left literal. + */ + private static boolean isSafeExternalExpression(String expression) { + return expression.startsWith("java.") + || expression.startsWith("os.") + || expression.startsWith("maven.") + || "file.separator".equals(expression) + || "path.separator".equals(expression) + || "line.separator".equals(expression) + || "revision".equals(expression) + || "changelist".equals(expression) + || "sha1".equals(expression); + } + protected List createPostProcessors( final Model model, final File projectDir, final ModelBuildingRequest config) { List processors = new ArrayList<>(2); diff --git a/maven-model-builder/src/test/java/org/apache/maven/model/interpolation/AbstractModelInterpolatorTest.java b/maven-model-builder/src/test/java/org/apache/maven/model/interpolation/AbstractModelInterpolatorTest.java index f95ba55d1213..67d5e9b11595 100644 --- a/maven-model-builder/src/test/java/org/apache/maven/model/interpolation/AbstractModelInterpolatorTest.java +++ b/maven-model-builder/src/test/java/org/apache/maven/model/interpolation/AbstractModelInterpolatorTest.java @@ -358,6 +358,91 @@ public void testEnvars() throws Exception { assertEquals("/path/to/home", out.getProperties().getProperty("outputDirectory")); } + @Test + public void testMinimalValidationInterpolationUsesRestrictedPropertySet() throws Exception { + Properties context = new Properties(); + context.put("env.SOME_VAR", "some-value"); + context.put("some.property", "other-value"); + context.put("java.version", "21"); + + Model model = new Model(); + + Properties modelProperties = new Properties(); + modelProperties.setProperty("envDir", "${env.SOME_VAR}"); + modelProperties.setProperty("propDir", "${some.property}"); + modelProperties.setProperty("jdk", "${java.version}"); + + model.setProperties(modelProperties); + + ModelInterpolator interpolator = createInterpolator(); + + final SimpleProblemCollector collector = new SimpleProblemCollector(); + ModelBuildingRequest config = createModelBuildingRequest(context); + config.setValidationLevel(ModelBuildingRequest.VALIDATION_LEVEL_MINIMAL); + Model out = interpolator.interpolateModel(model, new File("."), config, collector); + assertProblemFree(collector); + + // At minimal validation level (the level used for models built while resolving + // dependency, parent and BOM-import POMs) env and arbitrary system/user properties + // stay literal... + assertEquals("${env.SOME_VAR}", out.getProperties().getProperty("envDir")); + assertEquals("${some.property}", out.getProperties().getProperty("propDir")); + // ...while JVM-defined and other well-known expressions keep resolving. + assertEquals("21", out.getProperties().getProperty("jdk")); + } + + @Test + public void testFullInterpolationOptOutRestoresPreviousBehaviorAtMinimalValidationLevel() throws Exception { + Properties context = new Properties(); + context.put("env.SOME_VAR", "some-value"); + context.put(AbstractStringBasedModelInterpolator.FULL_EXTERNAL_INTERPOLATION_PROPERTY, "true"); + + Model model = new Model(); + + Properties modelProperties = new Properties(); + modelProperties.setProperty("envDir", "${env.SOME_VAR}"); + + model.setProperties(modelProperties); + + ModelInterpolator interpolator = createInterpolator(); + + final SimpleProblemCollector collector = new SimpleProblemCollector(); + ModelBuildingRequest config = createModelBuildingRequest(context); + config.setValidationLevel(ModelBuildingRequest.VALIDATION_LEVEL_MINIMAL); + Model out = interpolator.interpolateModel(model, new File("."), config, collector); + assertProblemFree(collector); + + assertEquals("some-value", out.getProperties().getProperty("envDir")); + } + + @Test + public void testEnvarsStillInterpolatedAtStrictValidationLevel() throws Exception { + Properties context = new Properties(); + context.put("env.SOME_VAR", "some-value"); + context.put("some.property", "other-value"); + + Model model = new Model(); + + Properties modelProperties = new Properties(); + modelProperties.setProperty("envDir", "${env.SOME_VAR}"); + modelProperties.setProperty("propDir", "${some.property}"); + + model.setProperties(modelProperties); + + ModelInterpolator interpolator = createInterpolator(); + + final SimpleProblemCollector collector = new SimpleProblemCollector(); + ModelBuildingRequest config = createModelBuildingRequest(context); + config.setValidationLevel(ModelBuildingRequest.VALIDATION_LEVEL_STRICT); + Model out = interpolator.interpolateModel(model, new File("."), config, collector); + assertProblemFree(collector); + + // Operator project builds use strict validation and keep full interpolation, + // unchanged from previous behavior. + assertEquals("some-value", out.getProperties().getProperty("envDir")); + assertEquals("other-value", out.getProperties().getProperty("propDir")); + } + @Test public void testEnvarExpressionThatEvaluatesToNullReturnsTheLiteralString() throws Exception { Model model = new Model(); From 28b160ffd584da2e728dc6298a32903ada558cfe Mon Sep 17 00:00:00 2001 From: Sylwester Lachiewicz Date: Sun, 30 Aug 2026 14:40:32 +0200 Subject: [PATCH 2/3] Skip file and property profile activation for resolved dependency POMs Models built for dependency resolution (a dependency POM, one of its parents, or an imported BOM) are read at ModelBuildingRequest.VALIDATION_LEVEL_MINIMAL rather than the default STRICT level used for the project being built. For such models, file and property activation conditions are no longer evaluated, and profiles that do still activate (JDK version, operating system, activeByDefault) no longer contribute repositories or plugin repositories. Profiles supplied via settings or the command line are unaffected and keep activating as before. --- .../model/building/DefaultModelBuilder.java | 56 +++++++- ...solvedDependencyProfileActivationTest.java | 123 ++++++++++++++++++ 2 files changed, 176 insertions(+), 3 deletions(-) create mode 100644 maven-model-builder/src/test/java/org/apache/maven/model/building/ResolvedDependencyProfileActivationTest.java diff --git a/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultModelBuilder.java b/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultModelBuilder.java index 59161899ee8b..642caef72813 100644 --- a/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultModelBuilder.java +++ b/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultModelBuilder.java @@ -288,6 +288,11 @@ protected ModelBuildingResult build(ModelBuildingRequest request, Collection parentIds = new LinkedHashSet<>(); List lineage = new ArrayList<>(); + // Models built to resolve a dependency (a dependency POM, one of its parents, or an + // Models resolved for dependency, parent or BOM POMs evaluate only platform-derived + // activation (JDK, OS, activeByDefault). + boolean externalModel = isExternalModelBuildingRequest(request); + for (ModelData currentData = resultData; currentData != null; ) { lineage.add(currentData); @@ -307,8 +312,10 @@ protected ModelBuildingResult build(ModelBuildingRequest request, Collection interpolatedProfiles = getInterpolatedProfiles(rawModel, profileActivationContext, problems); tmpModel.setProfiles(interpolatedProfiles); - List activePomProfiles = - profileSelector.getActiveProfiles(tmpModel.getProfiles(), profileActivationContext, problems); + List activePomProfiles = profileSelector.getActiveProfiles( + externalModel ? withoutFileAndPropertyActivation(interpolatedProfiles) : tmpModel.getProfiles(), + profileActivationContext, + problems); List rawProfiles = new ArrayList<>(); for (Profile activePomProfile : activePomProfiles) { @@ -318,7 +325,11 @@ protected ModelBuildingResult build(ModelBuildingRequest request, Collection mutator) { return interpolatedActivations; } + /** + * Determines whether the given request builds a model to resolve a dependency, i.e. a POM + * read from a remote repository (a dependency POM, one of its parents, or an imported BOM) + * rather than a POM belonging to the project being built. Such requests use + * {@link ModelBuildingRequest#VALIDATION_LEVEL_MINIMAL}, see for instance + * {@code DefaultArtifactDescriptorReader#loadPom}; a project build uses + * {@link ModelBuildingRequest#VALIDATION_LEVEL_MAVEN_2_0} or higher. + */ + private static boolean isExternalModelBuildingRequest(ModelBuildingRequest request) { + return request.getValidationLevel() < ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_2_0; + } + + /** + * Returns the profiles from the given list whose activation does not depend on a file or a + * property. Profiles activated by JDK version, operating system, or marked + * {@code activeByDefault} are unaffected, since those conditions are a function of the build + * platform rather than of the model content. + */ + private static List withoutFileAndPropertyActivation(List profiles) { + List eligible = new ArrayList<>(profiles.size()); + for (Profile profile : profiles) { + Activation activation = profile.getActivation(); + if (activation == null || (activation.getFile() == null && activation.getProperty() == null)) { + eligible.add(profile); + } + } + return eligible; + } + + /** + * Returns a copy of the given profile with its repositories and plugin repositories cleared. + */ + private static Profile withoutRepositories(Profile profile) { + Profile stripped = profile.clone(); + stripped.setRepositories(Collections.emptyList()); + stripped.setPluginRepositories(Collections.emptyList()); + return stripped; + } + @Override public ModelBuildingResult build(ModelBuildingRequest request, ModelBuildingResult result) throws ModelBuildingException { diff --git a/maven-model-builder/src/test/java/org/apache/maven/model/building/ResolvedDependencyProfileActivationTest.java b/maven-model-builder/src/test/java/org/apache/maven/model/building/ResolvedDependencyProfileActivationTest.java new file mode 100644 index 000000000000..7ab4747d3f24 --- /dev/null +++ b/maven-model-builder/src/test/java/org/apache/maven/model/building/ResolvedDependencyProfileActivationTest.java @@ -0,0 +1,123 @@ +/* + * 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.model.building; + +import java.util.Properties; + +import org.apache.maven.model.Model; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Models built at {@link ModelBuildingRequest#VALIDATION_LEVEL_MINIMAL} come from POMs resolved + * from a repository during dependency resolution (a dependency POM, one of its parents, or an + * imported BOM), see for instance {@code DefaultArtifactDescriptorReader#loadPom}. Their file and + * property activators are not evaluated, and their profiles contribute no repositories. A project + * build, at {@link ModelBuildingRequest#VALIDATION_LEVEL_STRICT}, still evaluates every activator. + * Platform-derived activation (JDK version, operating system, activeByDefault) is unaffected at + * either level. + */ +class ResolvedDependencyProfileActivationTest { + + private static final String POM = "\n" + + " 4.0.0\n" + + " thegroup\n" + + " withprofiles\n" + + " 1\n" + + " pom\n" + + " \n" + + " \n" + + " file-condition\n" + + " \n" + + " \n" + + " ${some.dir}\n" + + " \n" + + " \n" + + " \n" + + " activated\n" + + " \n" + + " \n" + + " \n" + + " property-condition\n" + + " \n" + + " \n" + + " some.gating.property\n" + + " \n" + + " \n" + + " \n" + + " activated\n" + + " \n" + + " \n" + + " \n" + + " jdk-condition\n" + + " \n" + + " [1,)\n" + + " \n" + + " \n" + + " activated\n" + + " \n" + + " \n" + + " \n" + + " profile-repo\n" + + " https://repo.example.test/profile\n" + + " \n" + + " \n" + + " \n" + + " \n" + + "\n"; + + private Model build(int validationLevel) throws Exception { + ModelBuilder builder = new DefaultModelBuilderFactory().newInstance(); + + Properties systemProperties = new Properties(); + systemProperties.putAll(System.getProperties()); + systemProperties.setProperty("some.dir", System.getProperty("java.io.tmpdir")); + systemProperties.setProperty("some.gating.property", "true"); + + DefaultModelBuildingRequest request = new DefaultModelBuildingRequest(); + request.setModelSource(new StringModelSource(POM)); + request.setValidationLevel(validationLevel); + request.setSystemProperties(systemProperties); + + return builder.build(request).getEffectiveModel(); + } + + @Test + void testProjectBuildEvaluatesAllActivators() throws Exception { + Model model = build(ModelBuildingRequest.VALIDATION_LEVEL_STRICT); + + assertEquals("activated", model.getProperties().get("profile.file")); + assertEquals("activated", model.getProperties().get("profile.property")); + assertEquals("activated", model.getProperties().get("profile.jdk")); + assertTrue(model.getRepositories().stream().anyMatch(r -> "profile-repo".equals(r.getId()))); + } + + @Test + void testDependencyPomActivatesOnlyEnvironmentIndependentProfiles() throws Exception { + Model model = build(ModelBuildingRequest.VALIDATION_LEVEL_MINIMAL); + + assertNull(model.getProperties().get("profile.file")); + assertNull(model.getProperties().get("profile.property")); + assertEquals("activated", model.getProperties().get("profile.jdk")); + assertTrue(model.getRepositories().stream().noneMatch(r -> "profile-repo".equals(r.getId()))); + } +} From 632787d3c9ba2ad84bd7453eb26a3ceaa035d4e3 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Mon, 31 Aug 2026 10:03:09 +0200 Subject: [PATCH 3/3] Improve Javadoc for dependencyInterpolation.full opt-out property Document the behavior and default of FULL_EXTERNAL_INTERPOLATION_PROPERTY more clearly, and note its promotion to Constants with @Config in Maven 4.x for auto-generated configuration documentation. Co-Authored-By: Claude Opus 4.6 --- .../AbstractStringBasedModelInterpolator.java | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/maven-model-builder/src/main/java/org/apache/maven/model/interpolation/AbstractStringBasedModelInterpolator.java b/maven-model-builder/src/main/java/org/apache/maven/model/interpolation/AbstractStringBasedModelInterpolator.java index 58ed19c1371d..41399878eb8f 100644 --- a/maven-model-builder/src/main/java/org/apache/maven/model/interpolation/AbstractStringBasedModelInterpolator.java +++ b/maven-model-builder/src/main/java/org/apache/maven/model/interpolation/AbstractStringBasedModelInterpolator.java @@ -53,10 +53,17 @@ public abstract class AbstractStringBasedModelInterpolator implements ModelInterpolator { /** - * The name of the property that opts a build back into the previous behavior of - * interpolating models built at {@link ModelBuildingRequest#VALIDATION_LEVEL_MINIMAL} - * against the full set of session (system, environment and CLI) properties. Disabled - * by default. + * User property for opting back into the previous behavior of interpolating + * repository-resolved models (built at {@link ModelBuildingRequest#VALIDATION_LEVEL_MINIMAL}) + * against the full set of session properties (system, environment and CLI). + * When set to {@code "false"} (default), such models are interpolated only against + * their own {@code }, preventing property leaking from the requesting + * build into transitive POMs. When set to {@code "true"}, full interpolation is + * applied as in previous Maven versions. + *

+ * In Maven 4.x this constant is promoted to + * {@code org.apache.maven.api.Constants.MAVEN_MODEL_DEPENDENCY_INTERPOLATION_FULL} + * with {@code @Config} so it appears in the auto-generated configuration documentation. */ public static final String FULL_EXTERNAL_INTERPOLATION_PROPERTY = "maven.model.dependencyInterpolation.full";