Skip to content

Hosted Maven adds a dependencyManagement pin that the direct literal overrides when the dependency's groupId/artifactId is a ${property} or <exclusions> come first, so the build stays unpatched while scan reports redirected and VEX attests not_affected #683

Description

[agent] Found by the scheduled Maven bug-hunt routine (ledger #318).

Summary

The hosted Maven rewriter (scan --mode hosted) finds the patched dependency with find_maven_dependency_matches, which compares the raw text of the first <groupId> / <artifactId> inside each <dependency> block. Two ordinary pom shapes defeat that match:

  1. A property coordinate: <groupId>${ct.group}</groupId> (or <artifactId>${ct.artifact}</artifactId>) with the property defined in the same pom's <properties>.
  2. <exclusions> written before the dependency's own <groupId>. Maven accepts any child order, and the first <groupId> in the block is then the exclusion's.

In both cases the direct <dependency> with its literal <version>1.10.0</version> isn't recognised. versioned is empty, so the rewriter takes the transitive branch and adds a <dependencyManagement> entry pinning 1.10.0-socket.<hex8>. Maven applies the managed version only when a direct dependency has none, so the direct literal 1.10.0 wins and Maven resolves Central's unpatched jar. The fail-closed suffix never takes effect.

socket-patch still reports success:

  • scan --mode hosted --json: exit 0, redirect.redirected: 1, with only the redirect_maven_dep_management_added warning, whose detail ("has no literal <version> in pom.xml") is false.
  • The in-run --vex writes a not_affected statement ("Patched via Socket patch … (redirected)").
  • For the ${property} variants, a later standalone socket-patch vex on a fresh checkout also attests not_affected (exit 0). The VEX pom parser (formats/maven) resolves ${prop} only in <version>, not in groupId/artifactId, so it files the direct declaration under the GA ${ct.group}:commons-text and treats the managed suffixed pin as effective. For the exclusions-first variant the VEX parser handles the order correctly and refuses (patched_ref_invalid: "a direct <version> overrides <dependencyManagement>"), so only the scan envelope and the in-run VEX are wrong there.

Impact

The project builds the vulnerable upstream jar while the scan envelope, the in-run VEX and (for property coordinates) the standalone VEX all claim the CVE is mitigated. This silently defeats the "fail-closed" guarantee in docs/ecosystems.md.

Repro

Built from the real-Maven capstone crates/socket-patch-cli/tests/e2e_redirect_maven_build.rs, unchanged except that the warmed project's pom.xml is rewritten before scan --mode hosted (local, uncommitted variant). The checks: a fresh checkout with the fixture purged from the local repo, then maven-dependency-plugin:copy-dependencies through the stock settings.xml mirror for socket-patch-<uuid>, with the resolved jar's META-INF/NOTICE.txt checked for the patch marker.

Variant A (property groupId):

<properties>
  <ct.group>org.apache.commons</ct.group>
</properties>
<dependencies>
  <dependency>
    <groupId>${ct.group}</groupId>
    <artifactId>commons-text</artifactId>
    <version>1.10.0</version>
  </dependency>
</dependencies>

Variant B: the same with <artifactId>${ct.artifact}</artifactId>.
Variant C (exclusions first):

<dependency>
  <exclusions>
    <exclusion><groupId>org.example.none</groupId><artifactId>none</artifactId></exclusion>
  </exclusions>
  <groupId>org.apache.commons</groupId>
  <artifactId>commons-text</artifactId>
  <version>1.10.0</version>
</dependency>

The resulting pom after scan --mode hosted (variant A):

  <dependencies>
    <dependency>
      <groupId>${ct.group}</groupId>
      <artifactId>commons-text</artifactId>
      <version>1.10.0</version>          <!-- untouched -->
    </dependency>
  </dependencies>
  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-text</artifactId>
        <version>1.10.0-socket.4d5e6f70</version>   <!-- overridden by the direct literal -->
      </dependency>
    </dependencies>
  </dependencyManagement>

Then mvn dependency:copy-dependencies resolves commons-text-1.10.0.jar from Central, without the patch marker.

Expected vs actual

  • Expected (docs/ecosystems.md, "Fail-closed by version suffixing (hosted Maven)"; CLI_CONTRACT.md, the maven paragraph of the hosted rewriter): the rewriter "rewrites the literal <version>", and adds a <dependencyManagement> entry only "for a transitive / managed dependency with no literal version in your pom". Either the direct literal is suffixed, or the dependency is refused or warned (like redirect_maven_dep_unpinned for a ${property} version) with redirected: 0 and no VEX statement.
  • Actual: the direct literal is left alone, a shadowed dM pin is added, and the run reports redirected: 1 plus a not_affected VEX statement while Maven builds the unpatched jar.

Matrix (Linux, JDK 21, main 045d7ec)

Maven A: ${prop} groupId B: ${prop} artifactId C: exclusions before groupId literal control
3.6.3 fail (scan + in-run VEX + standalone VEX attest) fail (same) fail (scan + in-run VEX attest; standalone VEX refuses) —
3.9.11 fail ×2 fail fail pass (suffixed patched jar resolved)
3.9.16 fail fail fail —
4.0.0-rc-7 fail fail fail —

On every failing cell the fresh resolve linked commons-text-1.10.0.jar (unpatched). macOS and Windows weren't probed: the defect is pure pom-text matching with no OS-dependent path. It wasn't bisected; the matching code is unchanged in the v4.0.0 release.

Suspect code

  • crates/socket-patch-core/src/patch/redirect/mod.rs:6078: find_maven_dependency_matches takes the first <groupId> / <artifactId> in the block (exclusions included) and compares raw text without resolving ${…}.
  • crates/socket-patch-core/src/patch/redirect/mod.rs:6346: if versioned.is_empty() assumes "no match with a literal version" means transitive or managed. A direct dependency the matcher can't identify falls into this branch.
  • crates/socket-patch-core/src/formats/maven/mod.rs:92-98 and crates/socket-patch-core/src/vex/discover/maven.rs:591: VEX resolves properties for <version> only, so the property-coordinate variants are attested by standalone vex too.

Related, not duplicates: #655 is the same symptom in the vendored reactor planner (maven_reactor.rs, keyed_declarations), a separate code path. #513 covers an unresolved ${prop} version in the reactor. #259 covers comment/profile/plugin markup in the hosted rewriter.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions