File gradle-CVE-2023-35946.patch of Package gradle
From 859eae2b2acf751ae7db3c9ffefe275aa5da0d5d Mon Sep 17 00:00:00 2001
From: Louis Jacomet <louis@gradle.com>
Date: Thu, 15 Jun 2023 17:10:18 +0200
Subject: [PATCH] Fix dependency cache path traversal vulnerability
Gradle leverages the protection added to prevent ZipSlip for any path
computation inside a Gradle cache.
See https://github.com/gradle/gradle/security/advisories/GHSA-2h6c-rv6q-494v
---
.../local/DefaultPathKeyFileStore.java | 26 ++-
.../local/DefaultPathKeyFileStoreTest.groovy | 3 +-
.../CacheResolveIntegrationTest.groovy | 151 ++++++++++++++++++
3 files changed, 174 insertions(+), 6 deletions(-)
--- a/subprojects/core/src/main/java/org/gradle/internal/resource/local/DefaultPathKeyFileStore.java
+++ b/subprojects/core/src/main/java/org/gradle/internal/resource/local/DefaultPathKeyFileStore.java
@@ -26,11 +26,15 @@ import org.gradle.api.internal.file.dele
import org.gradle.internal.UncheckedException;
import org.gradle.util.GFileUtils;
import org.gradle.util.RelativePathUtil;
+import org.gradle.wrapper.PathTraversalChecker;
import java.io.File;
+import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
import static org.gradle.internal.FileUtils.hasExtension;
@@ -79,8 +83,17 @@ public class DefaultPathKeyFileStore imp
return saveIntoFileStore(source, getFile(path), false);
}
- private File getFile(String path) {
- return new File(baseDir, path);
+ private File getFile(String... path) {
+ String composedPath;
+ if (path.length == 1) {
+ composedPath = path[0];
+ } else {
+ // We need to ignore empty Strings as this is what "new File(parent, path)" was doing for "path" empty.
+ composedPath = Arrays.stream(path)
+ .filter(((Predicate<String>) String::isEmpty).negate())
+ .collect(Collectors.joining(File.separator));
+ }
+ return new File(baseDir, PathTraversalChecker.safePathName(trimLeadingSlash(composedPath)));
}
private File getFileWhileCleaningInProgress(String path) {
@@ -234,4 +247,10 @@ public class DefaultPathKeyFileStore imp
return new File(baseDir, path);
}
}
-}
+
+ private static String trimLeadingSlash(String composedPath) {
+ if (!composedPath.isEmpty() && composedPath.charAt(0) == '/') {
+ return composedPath.substring(1);
+ }
+ return composedPath;
+ }}
--- a/subprojects/core/src/test/groovy/org/gradle/internal/resource/local/DefaultPathKeyFileStoreTest.groovy
+++ b/subprojects/core/src/test/groovy/org/gradle/internal/resource/local/DefaultPathKeyFileStoreTest.groovy
@@ -78,7 +78,8 @@ class DefaultPathKeyFileStoreTest extend
def b = createFile("def")
when:
- store.move("a", a)
+ // leading slash does not mean absolute path
+ store.move("/a", a)
store.move("b", b)
then:
--- /dev/null
+++ b/subprojects/core/src/main/java/org/gradle/internal/resource/local/PathTraversalChecker.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2022 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.gradle.wrapper;
+
+import org.gradle.api.NonNullApi;
+
+import java.io.File;
+import java.util.Locale;
+
+import static java.lang.String.format;
+
+@NonNullApi
+public class PathTraversalChecker {
+
+ /**
+ * Checks the entry name for path traversal vulnerable sequences.
+ *
+ * This code is used for path traversal, ZipSlip and TarSlip detection.
+ *
+ * <b>IMPLEMENTATION NOTE</b>
+ * We do it this way instead of the way recommended in <a href="https://snyk.io/research/zip-slip-vulnerability"></a>
+ * for performance reasons, calling {@link File#getCanonicalPath()} is too expensive.
+ *
+ * @throws IllegalArgumentException if the entry contains vulnerable sequences
+ */
+ public static String safePathName(String name) {
+ if (isUnsafePathName(name)) {
+ throw new IllegalArgumentException(format("'%s' is not a safe archive entry or path name.", name));
+ }
+ return name;
+ }
+
+ public static boolean isUnsafePathName(String name) {
+ return name.isEmpty()
+ || name.startsWith("/")
+ || name.startsWith("\\")
+ || containsDirectoryNavigation(name)
+ || (name.contains(":") && isWindows());
+ }
+
+ private static boolean containsDirectoryNavigation(String name) {
+ if (!name.contains("..")) {
+ return false;
+ }
+ // We have a .. but if not before a file separator or at the end, it is OK
+ return name.endsWith("\\..")
+ || name.contains("..\\")
+ || name.endsWith("/..")
+ || name.contains("../");
+ }
+
+ private static boolean isWindows() {
+ return System.getProperty("os.name").toLowerCase(Locale.US).contains("windows");
+ }
+}