Skip to content

Hosted Gradle snippet silently builds the unpatched jar in any project using dependency locking (gradle.lockfile), on every Gradle major and OS #396

Description

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

Summary

In hosted mode, a Gradle project gets the redirect_gradle_manual_snippet warning. It says to paste an exclusiveContent { … includeVersion(g, a, "<base>-socket.<hex8>") } block and to "bump the … dependency declaration to version -socket. — exclusiveContent is fail-closed by repo exclusivity." When the project uses Gradle dependency locking, which is common for reproducible builds, following those instructions exactly still gives a successful build with the unpatched upstream jar. The existing gradle.lockfile entry (commons-text:1.10.0=…) becomes a {strictly 1.10.0} constraint, and it beats the bumped 1.10.0-socket.4d5e6f70 request:

runtimeClasspath
+--- org.apache.commons:commons-text:1.10.0-socket.4d5e6f70 -> 1.10.0
+--- org.apache.commons:commons-text:{strictly 1.10.0} -> 1.10.0 (c)

This happens in the default, STRICT and LENIENT lock modes alike, with exit 0 and no warning. Only a later --write-locks picks up the patched version, and neither the snippet nor docs/ecosystems.md mentions lockfiles.

This is separate from #347. There, a transitive base-version request wins version conflict resolution. Here there's no transitive at all: the lock state's strict constraint wins on its own. A fix for #347 (for example a strictly declaration) would turn this into a loud resolve failure. That's better, but the guidance would still need to say "re-run --write-locks".

Impact

In any locked Gradle build, the user sees success and follows the documented steps, but ships the vulnerable jar. The "fail-closed" claim doesn't hold.

Repro

This needs no Socket API. The snippet text is exactly what scan --mode hosted prints on main f6b7fb9, with the url pointed at a local maven2 repo that serves the patched jar at 1.10.0-socket.4d5e6f70. I checked it end to end on Linux with the real CLI output against a mock API.

echo "rootProject.name='app'" > settings.gradle
cat > build.gradle <<'EOF'
plugins { id 'java' }
repositories { mavenCentral() }
dependencyLocking { lockAllConfigurations() }        // same result with lockMode = LockMode.STRICT / LENIENT
dependencies { implementation 'org.apache.commons:commons-text:1.10.0' }
tasks.register('cp', Copy) { from configurations.runtimeClasspath; into 'build/cp' }
EOF
gradle dependencies --write-locks                     # the project's committed lock state
# follow redirect_gradle_manual_snippet: paste the snippet and bump the declaration
cat >> build.gradle <<'EOF'
repositories {
    exclusiveContent {
        forRepository {
            maven { url "file:///path/to/socket-repo" }
        }
        filter {
            includeVersion("org.apache.commons", "commons-text", "1.10.0-socket.4d5e6f70")
        }
    }
}
EOF
sed -i "s/commons-text:1.10.0'/commons-text:1.10.0-socket.4d5e6f70'/" build.gradle
gradle cp; ls build/cp        # exit 0, commons-text-1.10.0.jar (upstream, unpatched)
gradle dependencies --write-locks && gradle cp   # only now: commons-text-1.10.0-socket.4d5e6f70.jar (patched)

The control is the same script without the dependencyLocking line. It resolves the patched -socket jar in every cell.

Expected vs actual

  • Expected (docs/ecosystems.md, "Gradle (hosted Maven)"): with the snippet pasted and the declaration bumped, the build is "fail-closed by repository exclusivity". It should consume the patched jar or fail.
  • Actual: the build succeeds and uses the upstream jar whenever a gradle.lockfile exists.

Matrix (probe run https://github.com/SocketDev/socket-patch/actions/runs/36791121715 plus local runs)

OS Gradle 6.9.4 (JDK 11) 7.6.6 (JDK 17) 8.14.3 (JDK 21) 9.8.0 (JDK 21)
Linux fail ×3 lock modes fail ×3 fail ×3 (also local) fail ×3 (also local)
macOS fail ×3 fail ×3 fail ×3 fail ×3
Windows fail ×3 fail ×3 fail ×3 fail ×3
no-lock control pass everywhere pass pass pass

"fail" means exit 0 with commons-text-1.10.0.jar and no patch marker. It's the first release with the snippet, v4.0.0 (per ledger #319).

Suspect code

crates/socket-patch-core/src/patch/redirect/mod.rs:7139 (gradle_snippet) and the call site at :6683. The guidance ignores lock state. The rewriter already receives the project files, so it could detect gradle.lockfile / *.lockfile / gradle/dependency-locks/ and add a "re-run with --write-locks" step, ideally together with a strictly pin so a stale lock fails loudly instead of silently winning.

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions