Skip to content

Hosted Gradle exclusiveContent snippet is not fail-closed: a transitive request for the base version wins conflict resolution and the unpatched jar is used #347

Description

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

Summary

For a Gradle build, scan --mode hosted prints a redirect_gradle_manual_snippet warning. It tells the user to paste an exclusiveContent block and to bump the dependency to <base>-socket.<hex8>, and says this "is fail-closed by repo exclusivity". That only holds when nothing else in the graph asks for the artifact. Gradle's version ordering puts a version with an extra non-numeric part below the bare version (1.10.0-socket.4d5e6f70 < 1.10.0). So if any other dependency transitively requests the base version, conflict resolution silently picks upstream 1.10.0 from Maven Central. The build exits 0 with the unpatched jar on the classpath.

Impact

The documented guarantee is broken silently. Vulnerable artifacts are usually pulled in transitively (commons-text is a dependency of commons-configuration2, for example), so following the snippet's instructions exactly still ships the vulnerable bytes, and nothing shows it.

Repro

  1. Run scan --mode hosted against a mock API that returns a maven2 override with mavenSuffixedVersion = 1.10.0-socket.4d5e6f70, plus a warm MAVEN_REPO_LOCAL holding commons-text 1.10.0 (the only way discovery finds it; see the sibling issue on discovery). socket-patch prints this snippet (index URL replaced by a local file:// repo that serves the patched jar and the re-versioned pom):
plugins { id 'java' }
repositories { mavenCentral() }
repositories {
    exclusiveContent {
        forRepository {
            maven { url "file:///tmp/gw/socketrepo" }
        }
        filter {
            includeVersion("org.apache.commons", "commons-text", "1.10.0-socket.4d5e6f70")
        }
    }
}
dependencies {
    implementation 'org.apache.commons:commons-text:1.10.0-socket.4d5e6f70'   // bumped, as instructed
    implementation 'org.apache.commons:commons-configuration2:2.9.0'          // depends on commons-text 1.10.0
}
  1. gradle dependencyInsight --dependency commons-text --configuration runtimeClasspath:
org.apache.commons:commons-text:1.10.0
   Selection reasons:
      - By conflict resolution: between versions 1.10.0 and 1.10.0-socket.4d5e6f70
org.apache.commons:commons-text:1.10.0-socket.4d5e6f70 -> 1.10.0

runtimeClasspath holds commons-text-1.10.0.jar, the build exits 0, and there's no warning.

Expected vs actual

  • Expected (docs/ecosystems.md, "Gradle (hosted Maven)"): "It is fail-closed by repository exclusivity: the exclusiveContent filter routes only the suffixed version to the Socket repo, which is the only place it exists." The patched jar should be the one used, or the build should fail.
  • Actual: the suffixed version is silently upgraded away to the unpatched upstream 1.10.0.

Changing the declaration to a strict version fixes it on 9.8.0: implementation('org.apache.commons:commons-text') { version { strictly '1.10.0-socket.4d5e6f70' } } resolves commons-text-1.10.0-socket.4d5e6f70.jar. A constraints { … strictly … } block or a resolutionStrategy.force would also work, and those also cover the transitive-only case where the user has no declaration to bump.

Matrix (Linux; each cell run twice with the same result)

Gradle JDK Direct dep only Direct + transitive base request
6.9.4 11 patched ✅ unpatched, exit 0 ❌
7.6.6 17 patched ✅ unpatched, exit 0 ❌
8.14.3 21 patched ✅ unpatched, exit 0 ❌
9.8.0 21 patched ✅ unpatched, exit 0 ❌

Version ordering is OS-independent Gradle core behaviour, so macOS and Windows weren't probed.

First bad release

v4.0.0. The snippet doesn't exist in v3.3.0.

Suspect code

  • crates/socket-patch-core/src/patch/redirect/mod.rs:7139 (gradle_snippet): emits a plain declaration bump with no strictly / constraint.
  • docs/ecosystems.md "Gradle (hosted Maven)": makes the fail-closed claim.

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