mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2026-09-28 17:11:11 -09:00
Merge remote-tracking branch 'origin/GP-7206_ryanmkurtz_unzip'
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* 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 ghidra.util;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.*;
|
||||
import java.nio.file.attribute.PosixFilePermission;
|
||||
import java.util.*;
|
||||
|
||||
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
|
||||
import org.apache.commons.compress.archivers.zip.ZipFile;
|
||||
import org.apache.commons.compress.utils.InputStreamStatistics;
|
||||
|
||||
import ghidra.util.exception.CancelledException;
|
||||
import ghidra.util.task.TaskMonitor;
|
||||
import utilities.util.FileUtilities;
|
||||
|
||||
/**
|
||||
* A utility class to safely extract zip files.
|
||||
* <p>
|
||||
* Protects against:
|
||||
* <ul>
|
||||
* <li><a href="https://en.wikipedia.org/wiki/Directory_traversal_attack">Zip Slip</a></li>
|
||||
* <li><a href="https://en.wikipedia.org/wiki/Zip_bomb">Zip Bomb</a></li>
|
||||
*
|
||||
*/
|
||||
public class SecureZipExtractor {
|
||||
|
||||
private static final double MAX_THRESHOLD_RATIO = 100.0; // 100:1 ratio
|
||||
private static final long MAX_SINGLE_FILE_SIZE = 100 * 1024 * 1024; // 100 MB
|
||||
private static final long MAX_TOTAL_SIZE = 1024 * 1024 * 1024; // 1 GB
|
||||
|
||||
/**
|
||||
* Securely extracts the given zip file to the given directory
|
||||
*
|
||||
* @param zipFile The zip {@link File} to extract
|
||||
* @param targetDir The directory to unzip to
|
||||
* @param monitor A cancellable {@link TaskMonitor}
|
||||
* @throws IOException if a zip slip, zip bomb, or other IO-related error occurred
|
||||
* @throws CancelledException if the operation was cancelled
|
||||
*/
|
||||
public static void extractSecurely(File zipFile, File targetDir, TaskMonitor monitor)
|
||||
throws IOException, CancelledException {
|
||||
|
||||
try (ZipFile archive = ZipFile.builder().setFile(zipFile).get()) {
|
||||
long total = 0;
|
||||
Enumeration<ZipArchiveEntry> entries = archive.getEntries();
|
||||
|
||||
while (entries.hasMoreElements()) {
|
||||
monitor.checkCancelled();
|
||||
|
||||
ZipArchiveEntry entry = entries.nextElement();
|
||||
File outputFile = FileUtilities.getSecureFile(targetDir, entry.getName());
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
outputFile.mkdirs();
|
||||
}
|
||||
else {
|
||||
total += extractSecurely(archive, entry, outputFile.toPath(), total);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Securely writes the given zip entry to the given output file. Assumes that the output file's
|
||||
* directory exists.
|
||||
*
|
||||
* @param archive The original {@link ZipFile} to extract from
|
||||
* @param entry The {@link ZipArchiveEntry entry} to extract
|
||||
* @param outputFile The {@link File} to extract the entry to
|
||||
* @throws IOException if a zip bomb or other IO-related error occurred
|
||||
*/
|
||||
public static void extractSecurely(ZipFile archive, ZipArchiveEntry entry, File outputFile)
|
||||
throws IOException {
|
||||
extractSecurely(archive, entry, outputFile.toPath(), 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Securely writes the given zip entry to the given output path
|
||||
*
|
||||
* @param archive The original {@link ZipFile} to extract from
|
||||
* @param entry The {@link ZipArchiveEntry entry} to extract
|
||||
* @param outputPath The {@link Path} to extract the entry to
|
||||
* @param totalBytesExtracted The total number of bytes that have been extracted so far
|
||||
* @return The number of bytes extracted for this entry
|
||||
* @throws IOException if a zip bomb or other IO-related error occurred
|
||||
*/
|
||||
private static long extractSecurely(ZipFile archive, ZipArchiveEntry entry, Path outputPath,
|
||||
long totalBytesExtracted) throws IOException {
|
||||
|
||||
try (InputStream is = archive.getInputStream(entry)) {
|
||||
if (!(is instanceof InputStreamStatistics stats)) {
|
||||
throw new IOException(
|
||||
"Stream does not support InputStreamStatistics tracking.");
|
||||
}
|
||||
|
||||
try (OutputStream os = Files.newOutputStream(outputPath, StandardOpenOption.CREATE,
|
||||
StandardOpenOption.TRUNCATE_EXISTING)) {
|
||||
|
||||
byte[] buffer = new byte[4096];
|
||||
int bytesRead;
|
||||
|
||||
while ((bytesRead = is.read(buffer)) != -1) {
|
||||
os.write(buffer, 0, bytesRead);
|
||||
|
||||
long uncompressed = stats.getUncompressedCount();
|
||||
long compressed = stats.getCompressedCount();
|
||||
|
||||
// Check single file threshold
|
||||
if (uncompressed > MAX_SINGLE_FILE_SIZE) {
|
||||
throw new IOException(
|
||||
"Zip bomb detected: Single entry exceeds maximum allowed size.");
|
||||
}
|
||||
|
||||
// Check total extraction threshold
|
||||
if (totalBytesExtracted + uncompressed > MAX_TOTAL_SIZE) {
|
||||
throw new IOException(
|
||||
"Zip bomb detected: Total extraction size exceeds limit.");
|
||||
}
|
||||
|
||||
// Check compression ratio (only after sufficient data is read to avoid false positives)
|
||||
if (compressed > 1024) {
|
||||
double ratio = (double) uncompressed / compressed;
|
||||
if (ratio > MAX_THRESHOLD_RATIO) {
|
||||
throw new IOException(
|
||||
"Zip bomb detected: Compression ratio limits exceeded (" +
|
||||
ratio + ")");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update its permissions (supported only on UNIX platforms)
|
||||
if (entry.getPlatform() == ZipArchiveEntry.PLATFORM_UNIX) {
|
||||
int mode = entry.getUnixMode();
|
||||
if (mode != 0) { // 0 indicates non-unix platform
|
||||
Set<PosixFilePermission> perms = getPermissions(mode);
|
||||
try {
|
||||
Files.setPosixFilePermissions(outputPath, perms);
|
||||
}
|
||||
catch (UnsupportedOperationException e) {
|
||||
// ignore error...possibly on Windows
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return stats.getUncompressedCount();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts Unix permissions to a set of {@link PosixFilePermission}s.
|
||||
*
|
||||
* @param unixMode integer representation of file permissions
|
||||
* @return set of POSIX file permissions
|
||||
*/
|
||||
private static Set<PosixFilePermission> getPermissions(int unixMode) {
|
||||
|
||||
Set<PosixFilePermission> permissions = new HashSet<>();
|
||||
|
||||
if ((unixMode & 0400) != 0) {
|
||||
permissions.add(PosixFilePermission.OWNER_READ);
|
||||
}
|
||||
if ((unixMode & 0200) != 0) {
|
||||
permissions.add(PosixFilePermission.OWNER_WRITE);
|
||||
}
|
||||
if ((unixMode & 0100) != 0) {
|
||||
permissions.add(PosixFilePermission.OWNER_EXECUTE);
|
||||
}
|
||||
if ((unixMode & 0040) != 0) {
|
||||
permissions.add(PosixFilePermission.GROUP_READ);
|
||||
}
|
||||
if ((unixMode & 0020) != 0) {
|
||||
permissions.add(PosixFilePermission.GROUP_WRITE);
|
||||
}
|
||||
if ((unixMode & 0010) != 0) {
|
||||
permissions.add(PosixFilePermission.GROUP_EXECUTE);
|
||||
}
|
||||
if ((unixMode & 0004) != 0) {
|
||||
permissions.add(PosixFilePermission.OTHERS_READ);
|
||||
}
|
||||
if ((unixMode & 0002) != 0) {
|
||||
permissions.add(PosixFilePermission.OTHERS_WRITE);
|
||||
}
|
||||
if ((unixMode & 0001) != 0) {
|
||||
permissions.add(PosixFilePermission.OTHERS_EXECUTE);
|
||||
}
|
||||
|
||||
return permissions;
|
||||
}
|
||||
}
|
||||
@@ -16,20 +16,18 @@
|
||||
package ghidra.util.extensions;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.attribute.PosixFilePermission;
|
||||
import java.util.*;
|
||||
|
||||
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
|
||||
import org.apache.commons.compress.archivers.zip.ZipFile;
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import generic.jar.ResourceFile;
|
||||
import ghidra.framework.Application;
|
||||
import ghidra.util.Msg;
|
||||
import ghidra.util.SecureZipExtractor;
|
||||
import ghidra.util.exception.CancelledException;
|
||||
import ghidra.util.task.TaskMonitor;
|
||||
import utilities.util.FileUtilities;
|
||||
@@ -578,21 +576,8 @@ public class ExtensionUtils {
|
||||
return false;
|
||||
}
|
||||
|
||||
try (ZipFile zipFile = new ZipFile.Builder().setFile(file).get()) {
|
||||
|
||||
Enumeration<ZipArchiveEntry> entries = zipFile.getEntries();
|
||||
while (entries.hasMoreElements()) {
|
||||
monitor.checkCancelled();
|
||||
|
||||
ZipArchiveEntry entry = entries.nextElement();
|
||||
File destination = FileUtilities.getSecureFile(installDirRoot, entry.getName());
|
||||
if (entry.isDirectory()) {
|
||||
destination.mkdirs();
|
||||
}
|
||||
else {
|
||||
writeZipEntryToFile(zipFile, entry, destination);
|
||||
}
|
||||
}
|
||||
try {
|
||||
SecureZipExtractor.extractSecurely(file, installDirRoot, monitor);
|
||||
}
|
||||
catch (IOException e) {
|
||||
if (!FileUtilities.deleteDir(destinationFolder)) {
|
||||
@@ -620,73 +605,4 @@ public class ExtensionUtils {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void writeZipEntryToFile(ZipFile zFile, ZipArchiveEntry entry, File destination)
|
||||
throws IOException {
|
||||
try (OutputStream outputStream =
|
||||
new BufferedOutputStream(new FileOutputStream(destination))) {
|
||||
|
||||
// Create the file at the new location...
|
||||
IOUtils.copy(zFile.getInputStream(entry), outputStream);
|
||||
|
||||
// ...and update its permissions. But only continue if the zip was created on a unix
|
||||
//platform. If not, we cannot use the posix libraries to set permissions.
|
||||
if (entry.getPlatform() != ZipArchiveEntry.PLATFORM_UNIX) {
|
||||
return;
|
||||
}
|
||||
|
||||
int mode = entry.getUnixMode();
|
||||
if (mode != 0) { // 0 indicates non-unix platform
|
||||
Set<PosixFilePermission> perms = getPermissions(mode);
|
||||
try {
|
||||
Files.setPosixFilePermissions(destination.toPath(), perms);
|
||||
}
|
||||
catch (UnsupportedOperationException e) {
|
||||
// Need to catch this, as Windows does not support the posix call. This is not
|
||||
// an error, however, and should just silently fail.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts Unix permissions to a set of {@link PosixFilePermission}s.
|
||||
*
|
||||
* @param unixMode integer representation of file permissions
|
||||
* @return set of POSIX file permissions
|
||||
*/
|
||||
private static Set<PosixFilePermission> getPermissions(int unixMode) {
|
||||
|
||||
Set<PosixFilePermission> permissions = new HashSet<>();
|
||||
|
||||
if ((unixMode & 0400) != 0) {
|
||||
permissions.add(PosixFilePermission.OWNER_READ);
|
||||
}
|
||||
if ((unixMode & 0200) != 0) {
|
||||
permissions.add(PosixFilePermission.OWNER_WRITE);
|
||||
}
|
||||
if ((unixMode & 0100) != 0) {
|
||||
permissions.add(PosixFilePermission.OWNER_EXECUTE);
|
||||
}
|
||||
if ((unixMode & 0040) != 0) {
|
||||
permissions.add(PosixFilePermission.GROUP_READ);
|
||||
}
|
||||
if ((unixMode & 0020) != 0) {
|
||||
permissions.add(PosixFilePermission.GROUP_WRITE);
|
||||
}
|
||||
if ((unixMode & 0010) != 0) {
|
||||
permissions.add(PosixFilePermission.GROUP_EXECUTE);
|
||||
}
|
||||
if ((unixMode & 0004) != 0) {
|
||||
permissions.add(PosixFilePermission.OTHERS_READ);
|
||||
}
|
||||
if ((unixMode & 0002) != 0) {
|
||||
permissions.add(PosixFilePermission.OTHERS_WRITE);
|
||||
}
|
||||
if ((unixMode & 0001) != 0) {
|
||||
permissions.add(PosixFilePermission.OTHERS_EXECUTE);
|
||||
}
|
||||
|
||||
return permissions;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,13 +17,13 @@ package generic.theme;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.Enumeration;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipFile;
|
||||
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
|
||||
import org.apache.commons.compress.archivers.zip.ZipFile;
|
||||
|
||||
import ghidra.framework.Application;
|
||||
import ghidra.util.Msg;
|
||||
import ghidra.util.SecureZipExtractor;
|
||||
import utilities.util.FileUtilities;
|
||||
|
||||
/**
|
||||
@@ -70,18 +70,18 @@ class ThemeReader extends AbstractThemeReader {
|
||||
}
|
||||
|
||||
private GTheme readZipTheme() throws IOException {
|
||||
try (ZipFile zipFile = new ZipFile(file)) {
|
||||
Enumeration<? extends ZipEntry> entries = zipFile.entries();
|
||||
try (ZipFile archive = ZipFile.builder().setFile(file).get()) {
|
||||
Enumeration<ZipArchiveEntry> entries = archive.getEntries();
|
||||
while (entries.hasMoreElements()) {
|
||||
ZipEntry entry = entries.nextElement();
|
||||
ZipArchiveEntry entry = entries.nextElement();
|
||||
String name = entry.getName();
|
||||
try (InputStream is = zipFile.getInputStream(entry)) {
|
||||
if (name.endsWith(".theme")) {
|
||||
if (name.endsWith(".theme")) {
|
||||
try (InputStream is = archive.getInputStream(entry)) {
|
||||
processThemeData(name, is);
|
||||
}
|
||||
else {
|
||||
processIconFile(name, is);
|
||||
}
|
||||
}
|
||||
else {
|
||||
processIconFile(name, entry, archive);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -131,7 +131,8 @@ class ThemeReader extends AbstractThemeReader {
|
||||
"Custom sections not allowed in theme files! " + section.getName());
|
||||
}
|
||||
|
||||
private void processIconFile(String path, InputStream is) throws IOException {
|
||||
private void processIconFile(String path, ZipArchiveEntry entry, ZipFile archive)
|
||||
throws IOException {
|
||||
int indexOf = path.indexOf("images/");
|
||||
if (indexOf < 0) {
|
||||
Msg.error(this, "Unknown file: " + path);
|
||||
@@ -147,7 +148,8 @@ class ThemeReader extends AbstractThemeReader {
|
||||
String relativePath = path.substring(indexOf, path.length());
|
||||
File dir = Application.getUserSettingsDirectory();
|
||||
File iconFile = FileUtilities.getSecureFile(dir, relativePath);
|
||||
FileUtils.copyInputStreamToFile(is, iconFile);
|
||||
iconFile.getParentFile().mkdirs();
|
||||
SecureZipExtractor.extractSecurely(archive, entry, iconFile);
|
||||
}
|
||||
|
||||
private void processThemeData(String name, InputStream is) throws IOException {
|
||||
|
||||
Reference in New Issue
Block a user