Headline
CVE-2023-35946: Fix dependency cache path traversal vulnerability · gradle/gradle@859eae2
Gradle is a build tool with a focus on build automation and support for multi-language development. When Gradle writes a dependency into its dependency cache, it uses the dependency’s coordinates to compute a file location. With specially crafted dependency coordinates, Gradle can be made to write files into an unintended location. The file may be written outside the dependency cache or over another file in the dependency cache. This vulnerability could be used to poison the dependency cache or overwrite important files elsewhere on the filesystem where the Gradle process has write permissions. Exploiting this vulnerability requires an attacker to have control over a dependency repository used by the Gradle build or have the ability to modify the build’s configuration. It is unlikely that this would go unnoticed. A fix has been released in Gradle 7.6.2 and 8.2 to protect against this vulnerability. Gradle will refuse to cache dependencies that have path traversal elements in their dependency coordinates. It is recommended that users upgrade to a patched version. If you are unable to upgrade to Gradle 7.6.2 or 8.2, dependency verification
will make this vulnerability more difficult to exploit.
Expand Up @@ -20,6 +20,8 @@ import org.gradle.cache.internal.scopes.DefaultCacheScopeMapping import org.gradle.integtests.fixtures.AbstractHttpDependencyResolutionTest import org.gradle.integtests.fixtures.ToBeFixedForConfigurationCache import org.gradle.integtests.fixtures.cache.CachingIntegrationFixture import org.gradle.internal.hash.Hashing import org.gradle.test.fixtures.file.TestFile
import java.nio.file.Files
Expand Down Expand Up @@ -153,6 +155,155 @@ task listJars { succeeds(‘listJars’) }
def ‘cannot write cache entries outside of GAV’() { given: def fakeDep = temporaryFolder.testDirectory.file(‘fake-repo/pwned.txt’) fakeDep << “"” Hello world! “"” def hash = Hashing.sha1().hashFile(fakeDep).toString() def hashOfBootJar = ‘1234’ // for demo purpose def invalidPath = “org.spring/core/1.0/$hash/artifact-1.0./…/…/…/…/boot/2.0/$hashOfBootJar/pwned.txt” def invalidLocation = executer.gradleUserHomeDir.file(cachePath + invalidPath).canonicalFile
server.allowGetOrHead("/repo/org/boot/2.0/$hashOfBootJar/pwned.txt", fakeDep)
and: withValidJavaSource() buildWithJavaLibraryAndMavenRepoArtifactOnly()
and: buildFile << “"” dependencies { implementation ‘org.spring:core:1.0@/…/…/…/…/boot/2.0/$hashOfBootJar/pwned.txt’ } “"”
when: fails(‘compileJava’)
then: failureCauseContains(‘is not a safe zip entry name’) // If the build did not fail, Gradle would effectively write a file inside org.spring/boot/2.0 instead of inside org.spring/core/1.0 // If we have the real hash of a JAR in those other coordinates, Gradle could overwrite and replace the real JAR with a malicious one !invalidLocation.exists() }
def ‘cannot write cache entries outside of dependency cache’() { given: def fakeDep = temporaryFolder.testDirectory.file(‘fake-repo/pwned.txt’) fakeDep << “"” Hello world! “"” // Code block used to verify what happens if the build succeeds def hash = Hashing.sha1().hashFile(fakeDep).toString() def invalidPath = “org.spring/…/…/…/…/…/core/1.0/$hash/artifact-1.0./…/…/…/…/.ssh/pwned.txt” def invalidLocation = executer.gradleUserHomeDir.file(cachePath + invalidPath).canonicalFile
server.allowGetOrHead('/repo/org/.ssh/pwned.txt’, fakeDep)
and: withValidJavaSource() buildWithJavaLibraryAndMavenRepoArtifactOnly()
and: buildFile << “"” dependencies { implementation ‘org.spring/…/…/…/…/…/:core:1.0@/…/…/…/…/.ssh/pwned.txt’ } “"”
when: fails(‘compileJava’)
then: failureCauseContains(‘is not a safe zip entry name’) // If the build did not fail, Gradle would effectively write a file inside a folder that is a sibling to the Gradle User Home // If this was ~/.gradle, Gradle would have written in ~/.ssh !invalidLocation.exists() }
def ‘cannot write cache entries anywhere on disk using metadata’() { given: // Our crafty coordinates def pwnedDep = mavenRepo.module('org.spring/…/…/…/…/…/’, ‘core’) // Our abused coordinates that will see a POM request def abusedCoordinates = mavenHttpRepo.module('org.spring’, 'core’, ‘1.0’).publish() // Defeat the Gradle validation that will verify metadata content match requested coordinates abusedCoordinates.pom.file.replace('<groupId>org.spring</groupId>’, ‘<groupId>org.spring/…/…/…/…/…/</groupId>’) // Our test dependency that now has a crafty dependency itself def testDep = mavenHttpRepo.module('org.test’, ‘test’).dependsOn(pwnedDep, type: ‘/…/…/…/…/.ssh/pwned.txt’).publish()
def fakeDep = temporaryFolder.testDirectory.file(‘fake-repo/pwned.txt’) fakeDep << “"” Hello world! “"” def hash = Hashing.sha1().hashFile(fakeDep).toString() def invalidPath = “org.spring/…/…/…/…/…/core/1.0/$hash/artifact-1.0./…/…/…/…/.ssh/pwned.txt” def invalidLocation = executer.gradleUserHomeDir.file(cachePath + invalidPath).canonicalFile
testDep.allowAll() abusedCoordinates.allowAll() server.allowGetOrHead('/repo/org/.ssh/pwned.txt’, fakeDep)
and: withValidJavaSource() buildWithJavaLibraryAndMavenRepo()
and: buildFile << “"” dependencies { implementation ‘org.test:test:1.0’ } “"”
when: fails(‘compileJava’)
then: failureCauseContains(‘is not a safe zip entry name’) // If the build did not fail, Gradle would effectively write a file inside a folder that is a sibling to the Gradle User Home // If this was ~/.gradle, Gradle would have written in ~/.ssh !invalidLocation.exists() }
private String getCachePath() { “caches/${CacheLayout.ROOT.key}/${CacheLayout.FILE_STORE.key}/” }
private void buildWithJavaLibraryAndMavenRepoArtifactOnly() { buildFile << “"” plugins { id(‘java-library’) } repositories { maven { url “${mavenHttpRepo.uri}” metadataSources { artifact() } } } “"” }
private void buildWithJavaLibraryAndMavenRepo() { buildFile << “"” plugins { id(‘java-library’) } repositories { maven { url “${mavenHttpRepo.uri}” } } “"” }
private TestFile withValidJavaSource() { temporaryFolder.testDirectory.file(‘src/main/java/org/test/Base.java’) << “"” package org.test; public class Base {} “"” }
def relocateCachesAndChangeGradleHome() { def otherHome = executer.gradleUserHomeDir.parentFile.createDir(‘other-home’) def otherCacheDir = otherHome.toPath().resolve(DefaultCacheScopeMapping.GLOBAL_CACHE_DIR_NAME) Expand Down