Restrict what a repository-resolved model contributes to the build - #12948
Restrict what a repository-resolved model contributes to the build#12948slachiewicz wants to merge 14 commits into
Conversation
3bf1265 to
b9f115a
Compare
Repositories contributed by a model resolved from a repository are merged recessively; repositories supplied by the request or session keep precedence. Gates the replace branch of ModelBuilderSessionState.mergeRepositories() on isBuildRequest(), so only filesystem/project models (BUILD_PROJECT, BUILD_EFFECTIVE, BUILD_CONSUMER) retain replace semantics.
Models built while resolving a dependency POM or one of its parents from a repository (ModelBuilderRequest.RequestType.CONSUMER_DEPENDENCY and CONSUMER_PARENT) 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. Models belonging to the project being built use the BUILD_* request types and are unaffected. The prior behavior can be restored globally with the new maven.model.dependencyInterpolation.full system/user property.
Extends the previous restriction to the deprecated compat model-builder interpolator (AbstractStringBasedModelInterpolator), which independently builds its own value-source list. Models built at ModelBuildingRequest.VALIDATION_LEVEL_MINIMAL are the models Maven builds while resolving dependency, parent and BOM-import POMs from a repository; such models now interpolate their user/system properties only against the same allowlist as the modern builder. Operator project builds use a higher validation level and are unaffected. Same maven.model.dependencyInterpolation.full opt-out applies here too.
…pendency POMs Models built to resolve a dependency (ModelBuilderRequest.RequestType.CONSUMER_DEPENDENCY) are read from a repository, not authored by the project being built. For such models, file, property, and condition 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, and models belonging to the project's own parent chain, are unaffected.
…s (compat) Extends the previous restriction to the deprecated compat model-builder (DefaultModelBuilder), which independently performs its own profile selection and injection. Models built at ModelBuildingRequest.VALIDATION_LEVEL_MINIMAL are the models Maven builds while resolving dependency, parent and BOM-import POMs from a repository. 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.
readAsParentModel() caches the parent model it builds keyed only by source and activation record. A dependency's own remote parent and a project's own remote parent can be the exact same POM source, and the two must not share that cache entry: only a request originating from CONSUMER_DEPENDENCY skips file, property, and condition activation and drops repositories, so a cache hit computed under one lineage must never be handed to the other. request.getRequestType() cannot carry that distinction by itself, because a parent lookup always derives a CONSUMER_PARENT request regardless of what kind of session triggered it. A new sticky externalOrigin flag is threaded through derive() instead -- seeded on CONSUMER_DEPENDENCY and OR'd forward through every derivation, surviving as many parent hops as the lineage has -- and readAsParentModel() and the shared getActiveProfiles() gate both key off it. The cache is partitioned into two tags accordingly, so the two lineages never share an entry for the same source. Internal to ModelBuilderSessionState; no public API change.
…tory POM A dependencyManagement import (scope=import or type=bom) reads its managed dependencies from whichever POM the coordinates resolve to. system scope and systemPath bind a dependency to a file on the local filesystem, which is only meaningful for projects belonging to the current build. Track, in doLoadDependencyManagement, whether the import POM was resolved from the reactor or from a repository. Managed dependencies declaring system scope or a systemPath in a repository-resolved import are dropped with a warning, and are removed from the cached model so a later import cannot reintroduce them. The previous behaviour remains available, also with a warning, behind the new maven.repository.dependencyManagement.allowSystemScope user property. Imports resolved from the local reactor are unaffected.
6bef3e0 to
9e78b2a
Compare
MavenITmng4379 and MavenITmng3586 cover a systemPath supplied through a property or an environment variable in a POM resolved from a repository; MavenITmng4590 covers dependency management imported from one. Those models now use restricted defaults, so each test requests the previous behaviour explicitly, which exercises the opt-out property, and gains a companion test asserting what the default does instead. Verified: mvn verify -Prun-its -Dits.test='MavenITmng4379*,MavenITmng4590*,MavenITmng3586*' -> 7 tests, 0 failures
9e78b2a to
27e4655
Compare
The static fixture pom used a POSIX-only systemPath, which File.isAbsolute() rejects on Windows; model validation then dropped the imported entry before the code under test ran, so the test's assertNull passed for the wrong reason. The test now writes the BOM into a per-test temporary remote repository with a systemPath computed from the current OS, and asserts that an ordinary managed dependency from the same import is present, so the system-scope assertions can no longer pass vacuously. The now-unused static fixture pom is removed.
DefaultProjectBuilder decided between BUILD_EFFECTIVE and CONSUMER_DEPENDENCY partly on the request's validation level. Validation strictness says how closely a model is checked, not whose model it is, and the 2.x project builder API asks for MAVEN_2_0 validation on a file the caller supplies, so such a build was labelled a consumer dependency. A non-null pomFile already marks a model backed by a file the caller pointed at -- a model resolved from a repository arrives as a resolved source with no pomFile -- so that alone now decides it. Covered by MavenITmng4765LocalPomProjectBuilderTest, which builds a local file through that API and reads back a command-line property.
This reverts commit dd8f0ca. RequestType is read for validation strictness as well as for provenance, so promoting every file-backed build to BUILD_EFFECTIVE turned lenient builds strict: DefaultMavenProjectBuilderTest.testPartialResultUponBadDependencyDeclaration, MavenProjectTest.testCloneWithDependencyManagement and PomConstructionTest.testDuplicateDependenciesCauseLastDeclarationToBePickedInLenientMode all depend on the previous behaviour.
The restriction on property interpolation and profile activation for a consumer-dependency or consumer-parent model now also requires the model source to be one Maven resolved from a repository (ModelSource.getModelId() non-null). A POM the caller hands to Maven as a file arrives with the same request type but is not repository-resolved, so it keeps full interpolation and activation. Integration-level cover: MavenITmng4765LocalPomProjectBuilderTest.
gnodet
left a comment
There was a problem hiding this comment.
✅ Well-Designed Security Hardening
Well-structured behavioral change with thorough test coverage and clear opt-out mechanisms. This is the master sibling of #12943 (maven-4.0.x), carrying the same logic with consistent implementations across compat and impl paths.
The test coverage is excellent: unit tests cover profile activation filtering, interpolation restriction, cache partitioning, externalOrigin propagation through grandparent hops, caller-supplied model exemption, and the opt-out property; integration tests cover both the new default and the opt-out for MNG-4379, MNG-4590, and MNG-3586.
Two observations (non-blocking, same as 4.0.x review):
-
Configuration documentation gap (
DefaultModelInterpolator.java:67): Themaven.model.dependencyInterpolation.fullopt-out property is declared as a local constant in two implementation classes, unlike its companionmaven.repository.dependencyManagement.allowSystemScopewhich is inConstants.javawith@Config. This means it won't appear in Maven's generated configuration documentation, creating a discoverability gap for users who need the opt-out. Since this PR changes longstanding default behavior (env-var interpolation in repository-resolved POMs, active since MNG-4379 in 2009), the opt-out should be as discoverable as possible. -
Future-proofing note (
DefaultModelBuilder.java:645): Thereplace && isBuildRequest()guard inmergeRepositoriesis correct for all current request types, but any future non-BUILD request type will implicitly receive recessive merging without an explicit decision. A brief code comment noting this design intent would help future maintainers.
Technical notes:
- The compat and impl implementations are consistent:
isSafeExternalExpressionis identical; compat correctly omitsconditionactivation filtering (doesn't exist in legacy model builder). externalOriginpropagation throughderive()is correctly sticky — a dependency's parent inherits the restriction.- The
ModelSource.getModelId() != nullapproach is cleaner than modifying request type classification, and preserves full interpolation for the 2.x project builder API path.
📋 PR Metadata
| Aspect | Current | Suggested |
|---|---|---|
| Milestone | (none) | 4.1.0 |
🔀 Backport Status
- ✅
maven-4.0.x— #12943 (OPEN, reviewed) - ⏳
maven-3.10.x— #12953 (OPEN) - ℹ️
maven-3.9.x— not targeted (appropriate for behavior-changing enhancement)
🤖 This review was generated by ForgeBot.
Move the maven.model.dependencyInterpolation.full property from local constants in DefaultModelInterpolator and AbstractStringBasedModelInterpolator to Constants.java with @config so it appears in Maven's auto-generated configuration documentation alongside its companion maven.repository.dependencyManagement.allowSystemScope. The compat module keeps a private mirror of the value because it does not depend on maven-api-core. Also add a design-intent comment on the replace && isBuildRequest() guard in DefaultModelBuilder.mergeRepositories to clarify that non-build request types intentionally use recessive merging. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Update DefaultModelInterpolatorTest to use Constants.MAVEN_MODEL_DEPENDENCY_INTERPOLATION_FULL and AbstractModelInterpolatorTest to use the string literal since the compat constant is now private. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
gnodet
left a comment
There was a problem hiding this comment.
Delta review — two new commits since the previous review address both observations:
-
✅ Configuration documentation gap → Fixed.
maven.model.dependencyInterpolation.fullis now promoted toConstants.javawith@Config(type = "java.lang.Boolean", defaultValue = "false"),@since 4.1.0, and comprehensive Javadoc. The compat module's duplicate is correctly madeprivatewith a cross-reference comment explaining why it can't import frommaven-api-core. Tests updated to useConstants.MAVEN_MODEL_DEPENDENCY_INTERPOLATION_FULL. -
✅ Future-proofing note → Fixed. Clear design-intent comment added to
mergeRepositoriesexplaining that future non-build RequestTypes intentionally fall through to recessive merging.
Both observations from the previous review are resolved. No new issues in the delta.
This review was generated by an AI agent and may contain inaccuracies. Please verify all suggestions before applying.
Claude Code on behalf of Guillaume Nodet
A model resolved from a repository should contribute less to the build than the project's own POM.
BUILD_*requests — the project being built — are unaffected.file,propertyorconditionare skipped, and profiles that do activate contribute no repositories. Settings and CLI profiles are untouched.java.*,os.*,maven.*, the three separators, and the CI-friendlyrevision,changelistandsha1. Everything else, environment variables included, is left as a literal expression.maven.model.dependencyInterpolation.full=truerestores the previous behaviour.systemscope in imported dependency management. Dependency management imported from a POM resolved from a repository no longer contributes entries that declaresystemscope or asystemPath; those entries are dropped and a warning names them. Management imported from the reactor is unaffected, and so is asystemscope declared by the project's own POM.maven.repository.dependencyManagement.allowSystemScope=truerestores the previous behaviour.The last two change behaviour that has been in place for a long time, which is why each ships an opt-out property.
MavenITmng4379,MavenITmng4590andMavenITmng3586keep their original assertions behind the opt-out and each gains a companion test asserting what the new default does instead.MavenITmng3586#testitFromProject, where the project's own POM declares thesystemscope, needs no opt-out.Worth a maintainer's eye on the interpolation default in particular: MNG-4379 has resolved a
systemPathfrom an environment variable in a repository-served POM since 2009, and under the new default such a build fails on the existing "must specify an absolute path" validation rather than resolving.Which of these applies is decided by the model's source kind rather than by the request type: a model
built from
Sources.resolvedSource(...)is one Maven fetched from a repository and reports a model id,while a POM handed to Maven as a file reports none.
MavenITmng4765LocalPomProjectBuilderTestcoversthe latter, and two unit tests assert a caller-supplied model is left alone. Keying on the request type
alone would not work: a POM supplied through the 2.x project builder API arrives with the same type as a
resolved dependency.
Each change is a separate commit.
Draft while related code paths are reviewed.