Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
import org.apache.maven.artifact.versioning.InvalidVersionSpecificationException;
import org.apache.maven.artifact.versioning.VersionRange;
import org.apache.maven.model.Activation;
import org.apache.maven.model.ActivationProperty;
import org.apache.maven.model.Build;
import org.apache.maven.model.Dependency;
import org.apache.maven.model.DependencyManagement;
Expand Down Expand Up @@ -522,18 +523,50 @@ private static boolean isExternalModelBuildingRequest(ModelBuildingRequest reque
* 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.
* <p>
* An exception is made for <em>negated</em> property activation ({@code <name>!foo</name>}
* with no {@code <value>}): such a profile fires when the property is <em>absent</em> and is
* therefore on by default. It cannot be injected by supplying a property — only suppressed —
* which is the safer direction. Filtering it out silently breaks models that rely on the
* common "opt-out flag" pattern (e.g. {@code resteasy-default} in JBoss/RESTEasy projects).
* The existing repository-stripping step still applies, preserving the security property that
* external models cannot inject new repositories.
*/
private static List<Profile> withoutFileAndPropertyActivation(List<Profile> profiles) {
List<Profile> eligible = new ArrayList<>(profiles.size());
for (Profile profile : profiles) {
Activation activation = profile.getActivation();
if (activation == null || (activation.getFile() == null && activation.getProperty() == null)) {
if (activation == null
|| (activation.getFile() == null && isSafePropertyActivation(activation.getProperty()))) {
eligible.add(profile);
}
}
return eligible;
}

/**
* Returns {@code true} if the given property activation condition is safe for use in external
* model builds — i.e. it cannot be toggled <em>on</em> by a user-supplied {@code -D} property.
* <p>
* A negated property condition ({@code <name>!foo</name>}, no value) activates when the
* property is absent, making it on by default. An attacker can only suppress it (by setting
* the property), not inject it. All other property conditions (positive name, or a required
* value) can be forced on externally and are therefore unsafe.
*
* @param prop the property activation element, or {@code null} if none
* @return {@code true} if the condition is absent or is a negated-name-only condition
*/
private static boolean isSafePropertyActivation(ActivationProperty prop) {
if (prop == null) {
return true; // no property condition — always safe
}
String name = prop.getName();
// "!foo" with no value = active when 'foo' is absent = default-on, cannot be injected
return name != null
&& name.startsWith("!")
&& (prop.getValue() == null || prop.getValue().isEmpty());
}

/**
* Returns a copy of the given profile with its repositories and plugin repositories cleared.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
*/
package org.apache.maven.model.building;

import java.util.List;

import org.apache.maven.model.Dependency;
import org.apache.maven.model.Parent;
import org.apache.maven.model.Repository;
Expand All @@ -26,8 +28,10 @@
import org.apache.maven.model.resolution.UnresolvableModelException;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

/**
*/
Expand Down Expand Up @@ -99,6 +103,67 @@ public ModelSource resolveModel(Dependency dependency) throws UnresolvableModelE
}
}

/**
* Verifies that a profile activated by a <em>negated</em> property condition
* ({@code <name>!foo</name>}, no value) is <em>not</em> filtered out during external model
* builds (VALIDATION_LEVEL_MINIMAL). Such profiles are on by default — they fire when the
* property is absent — and blocking them causes missing dependency versions in the effective
* model, reproducing the regression reported in
* <a href="https://github.com/apache/maven/issues/13084">GH-13084</a>.
*/
@Test
void negatedPropertyActivatedProfileIsPreservedInExternalModelBuild() throws Exception {
// A POM whose default profile ("my-default") activates when "skip.defaults" is absent.
// The profile provides the version for a dependency that has no version outside the profile.
String pom = "<project>\n"
+ " <modelVersion>4.0.0</modelVersion>\n"
+ " <groupId>org.example</groupId>\n"
+ " <artifactId>mylib</artifactId>\n"
+ " <version>1.0</version>\n"
+ " <packaging>jar</packaging>\n"
+ " <profiles>\n"
+ " <profile>\n"
+ " <id>my-default</id>\n"
+ " <activation>\n"
+ " <property>\n"
+ " <name>!skip.defaults</name>\n"
+ " </property>\n"
+ " </activation>\n"
+ " <dependencies>\n"
+ " <dependency>\n"
+ " <groupId>org.example</groupId>\n"
+ " <artifactId>dep-a</artifactId>\n"
+ " <version>2.0</version>\n"
+ " </dependency>\n"
+ " </dependencies>\n"
+ " </profile>\n"
+ " </profiles>\n"
+ "</project>\n";

ModelBuilder builder = new DefaultModelBuilderFactory().newInstance();
DefaultModelBuildingRequest request = new DefaultModelBuildingRequest();
request.setModelSource(new StringModelSource(pom));
// External model build: VALIDATION_LEVEL_MINIMAL is used by the artifact descriptor reader
request.setValidationLevel(ModelBuildingRequest.VALIDATION_LEVEL_MINIMAL);
request.setModelResolver(new BaseModelResolver());

ModelBuildingResult result = builder.build(request);
List<org.apache.maven.model.Dependency> deps =
result.getEffectiveModel().getDependencies();

assertTrue(
deps.stream().anyMatch(d -> "dep-a".equals(d.getArtifactId())),
"dep-a must be present: negated-property profile must activate in external model builds");
assertEquals(
"2.0",
deps.stream()
.filter(d -> "dep-a".equals(d.getArtifactId()))
.findFirst()
.map(org.apache.maven.model.Dependency::getVersion)
.orElse(null),
"dep-a version must be 2.0 (injected by the negated-property profile)");
}

static class BaseModelResolver implements ModelResolver {
@Override
public ModelSource resolveModel(String groupId, String artifactId, String version)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
import org.apache.maven.api.di.Singleton;
import org.apache.maven.api.feature.Features;
import org.apache.maven.api.model.Activation;
import org.apache.maven.api.model.ActivationProperty;
import org.apache.maven.api.model.Dependency;
import org.apache.maven.api.model.DependencyManagement;
import org.apache.maven.api.model.DeploymentRepository;
Expand Down Expand Up @@ -1757,11 +1758,29 @@ private List<Profile> getActiveProfiles(
*/
private static boolean hasFileOrPropertyOrConditionActivation(Profile profile) {
Activation activation = profile.getActivation();
return activation != null
&& (activation.getFile() != null
|| activation.getProperty() != null
|| (activation.getCondition() != null
&& !activation.getCondition().isBlank()));
if (activation == null) {
return false;
}
if (activation.getFile() != null) {
return true;
}
if (activation.getCondition() != null && !activation.getCondition().isBlank()) {
return true;
}
// A negated-name-only property activation ("!foo", no value) fires when the property is
// absent — it is on by default and can only be suppressed, not injected. Allow it through
// so that models relying on the common "opt-out flag" pattern (e.g. resteasy-default in
// JBoss projects) continue to work in external model builds. All other property conditions
// (positive name, or a required value) can be forced on via -D and remain blocked.
ActivationProperty prop = activation.getProperty();
if (prop == null) {
return false;
}
String name = prop.getName();
boolean negatedAbsenceCheck = name != null
&& name.startsWith("!")
&& (prop.getValue() == null || prop.getValue().isEmpty());
return !negatedAbsenceCheck;
}

/**
Expand Down
Loading