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/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..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 @@ -51,6 +51,22 @@ * @author jdcasey Created on Feb 3, 2005 */ public abstract class AbstractStringBasedModelInterpolator implements ModelInterpolator { + + /** + * 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"; + private static final List PROJECT_PREFIXES = Arrays.asList("pom.", "project."); private static final Collection TRANSLATED_PATH_EXPRESSIONS; @@ -180,21 +196,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 +233,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/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()))); + } +} 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();