Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion RNZipArchive.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@ Pod::Spec.new do |s|

s.dependency 'React-Core'
s.dependency 'SSZipArchive', '~>2.5.5'
s.pod_target_xcconfig = {
'HEADER_SEARCH_PATHS' => '$(inherited) "$(PODS_ROOT)/SSZipArchive" "$(PODS_ROOT)/SSZipArchive/SSZipArchive/minizip"'
}

s.subspec 'Core' do |ss|
ss.source_files = 'ios/*.{h,m}'
ss.source_files = 'ios/*.{h,m,mm}'
ss.public_header_files = ['ios/RNZipArchive.h']
end
end
70 changes: 28 additions & 42 deletions android/src/main/java/com/rnziparchive/RNZipArchiveModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@
import net.lingala.zip4j.model.enums.CompressionLevel;
import net.lingala.zip4j.model.enums.EncryptionMethod;
import net.lingala.zip4j.model.enums.AesKeyStrength;
import net.lingala.zip4j.progress.ProgressMonitor;

import java.nio.charset.Charset;

Expand Down Expand Up @@ -80,6 +79,7 @@ public void run() {
zipFile.setPassword(password.toCharArray());
} else {
promise.reject("RNZipArchiveError", String.format("Zip file: %s is not password protected", zipFilePath));
return;
}

List fileHeaderList = zipFile.getFileHeaders();
Expand All @@ -90,16 +90,10 @@ public void run() {
for (int i = 0; i < totalFiles; i++) {
FileHeader fileHeader = (FileHeader) fileHeaderList.get(i);

File fout = new File(destDirectory, fileHeader.getFileName());
String canonicalPath = fout.getCanonicalPath();
String destDirCanonicalPath = (new File(destDirectory).getCanonicalPath()) + File.separator;

if (!canonicalPath.startsWith(destDirCanonicalPath)) {
throw new SecurityException(String.format("Found Zip Path Traversal Vulnerability with %s", canonicalPath));
}
ZipSecurity.validateExtractPath(destDirectory, fileHeader.getFileName());

if (!fileHeader.isDirectory()) {
zipFile.extractFile(fileHeader, destDirectory);
zipFile.extractFile(fileHeader, destDirectory, ZipSecurity.createExtractParameters());
extractedFileNames.add(fileHeader.getFileName());
}
updateProgress(i + 1, totalFiles, zipFilePath);
Expand Down Expand Up @@ -127,10 +121,6 @@ public void run() {
}

try {
// Find the total uncompressed size of every file in the zip, so we can
// get an accurate progress measurement
final long totalUncompressedBytes = getUncompressedSize(zipFilePath, charset);

File destDir = new File(destDirectory);
if (!destDir.exists()) {
//noinspection ResultOfMethodCallIgnored
Expand All @@ -139,39 +129,40 @@ public void run() {

updateProgress(0, 1, zipFilePath); // force 0%

// We use arrays here so we can update values
// from inside the callback
final long[] extractedBytes = {0};
final int[] lastPercentage = {0};

net.lingala.zip4j.ZipFile zipFile = null;
net.lingala.zip4j.ZipFile zipFile;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
zipFile = new net.lingala.zip4j.ZipFile(zipFilePath);
zipFile.setCharset(Charset.forName(charset));
} else {
zipFile = new net.lingala.zip4j.ZipFile(zipFilePath);
}

ProgressMonitor progressMonitor = zipFile.getProgressMonitor();

zipFile.setRunInThread(true);
zipFile.extractAll(destDirectory);

while (!progressMonitor.getState().equals(ProgressMonitor.State.READY)) {
updateProgress(progressMonitor.getWorkCompleted(), progressMonitor.getTotalWork(), zipFilePath);

Thread.sleep(100);
List<FileHeader> fileHeaderList = zipFile.getFileHeaders();
long totalUncompressedBytes = 0;
for (FileHeader header : fileHeaderList) {
long size = header.getUncompressedSize();
if (size > 0) {
totalUncompressedBytes += size;
}
}
if (totalUncompressedBytes == 0) {
totalUncompressedBytes = 1;
}

if (progressMonitor.getResult().equals(ProgressMonitor.Result.SUCCESS)) {
zipFile.close();
updateProgress(1, 1, zipFilePath); // force 100%
promise.resolve(destDirectory);
} else if (progressMonitor.getResult().equals(ProgressMonitor.Result.ERROR)) {
throw new Exception("Error occurred. Error message: " + progressMonitor.getException().getMessage());
} else if (progressMonitor.getResult().equals(ProgressMonitor.Result.CANCELLED)) {
throw new Exception("Task cancelled");
long extractedBytes = 0;
for (FileHeader fileHeader : fileHeaderList) {
ZipSecurity.validateExtractPath(destDirectory, fileHeader.getFileName());
zipFile.extractFile(fileHeader, destDirectory, ZipSecurity.createExtractParameters());
long size = fileHeader.getUncompressedSize();
if (size > 0) {
extractedBytes += size;
}
updateProgress(extractedBytes, totalUncompressedBytes, zipFilePath);
}

zipFile.close();
updateProgress(1, 1, zipFilePath); // force 100%
promise.resolve(destDirectory);
} catch (Exception ex) {
updateProgress(0, 1, zipFilePath); // force 0%
promise.reject("RNZipArchiveError", "Failed to extract file " + ex.getLocalizedMessage());
Expand Down Expand Up @@ -238,12 +229,7 @@ public void run() {
Log.i("rnziparchive", "Extracting: " + entry.getName());

fout = new File(destDirectory, entry.getName());
String canonicalPath = fout.getCanonicalPath();
String destDirCanonicalPath = (new File(destDirectory).getCanonicalPath()) + File.separator;

if (!canonicalPath.startsWith(destDirCanonicalPath)) {
throw new SecurityException(String.format("Found Zip Path Traversal Vulnerability with %s", canonicalPath));
}
ZipSecurity.validateExtractPath(destDirectory, entry.getName());

if (!fout.exists()) {
//noinspection ResultOfMethodCallIgnored
Expand Down
44 changes: 44 additions & 0 deletions android/src/main/java/com/rnziparchive/ZipSecurity.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.rnziparchive;

import java.io.File;
import java.io.IOException;

import net.lingala.zip4j.model.UnzipParameters;

/**
* Validates zip extraction paths to prevent Zip Slip / path traversal attacks.
*/
public final class ZipSecurity {

private ZipSecurity() {
// utility class
}

/**
* Returns extraction parameters with symlink extraction disabled. zip4j enables symlink
* extraction by default but does not validate that a symlink's resolved target stays inside
* the destination directory, allowing archives to plant links escaping the extraction root
* (see issue #357). With symlinks disabled, zip4j skips symlink entries entirely.
*/
public static UnzipParameters createExtractParameters() {
UnzipParameters params = new UnzipParameters();
params.setExtractSymbolicLinks(false);
return params;
}

/**
* Ensures that extracting {@code entryName} into {@code destDirectory} would not escape the
* destination directory (e.g. via {@code ../} or absolute paths).
*/
public static void validateExtractPath(String destDirectory, String entryName) throws IOException {
File destDir = new File(destDirectory);
File fout = new File(destDir, entryName);

String canonicalPath = fout.getCanonicalPath();
String destDirCanonicalPath = destDir.getCanonicalPath() + File.separator;

if (!canonicalPath.startsWith(destDirCanonicalPath)) {
throw new SecurityException(String.format("Found Zip Path Traversal Vulnerability with %s", canonicalPath));
}
}
}
2 changes: 1 addition & 1 deletion ios/RNZipArchive.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#import <React/RCTBridgeModule.h>
#import <React/RCTEventEmitter.h>

@interface RNZipArchive : RCTEventEmitter<RCTBridgeModule, SSZipArchiveDelegate>
@interface RNZipArchive : RCTEventEmitter<RCTBridgeModule>

@property (nonatomic) NSString *processedFilePath;
@property (nonatomic) float progress;
Expand Down
Loading