Skip to content

Commit 23ec9a5

Browse files
authored
Fix Android ZIP path traversal (#8541)
1 parent a574294 commit 23ec9a5

6 files changed

Lines changed: 146 additions & 10 deletions

File tree

addons/ofxAndroid/Java/cc/openframeworks/OFZipUtil.java

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,49 @@ public class OFZipUtil
1616
{
1717
private static final int BUFFER_SIZE = 4096;
1818

19+
private static File resolveEntry(File outdir, String name) throws IOException
20+
{
21+
File canonicalOutdir = outdir.getCanonicalFile();
22+
File destination = new File(canonicalOutdir, name).getCanonicalFile();
23+
String outdirPath = canonicalOutdir.getPath();
24+
String destinationPath = destination.getPath();
25+
26+
if (!destinationPath.equals(outdirPath)
27+
&& !destinationPath.startsWith(outdirPath + File.separator))
28+
throw new IOException("Zip entry is outside the target directory: " + name);
29+
30+
return destination;
31+
}
32+
1933
public static void extractFile(ZipInputStream in, File outdir, String name) throws IOException
2034
{
2135
byte[] buffer = new byte[BUFFER_SIZE];
22-
BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(new File(outdir,name)));
23-
int count = -1;
24-
while ((count = in.read(buffer)) != -1)
25-
out.write(buffer, 0, count);
26-
out.close();
36+
File destination = resolveEntry(outdir, name);
37+
try (BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(destination)))
38+
{
39+
int count;
40+
while ((count = in.read(buffer)) != -1)
41+
out.write(buffer, 0, count);
42+
}
43+
}
44+
45+
private static void createDirectories(File outdir, String path) throws IOException
46+
{
47+
File directory = resolveEntry(outdir, path);
48+
if (!directory.exists() && !directory.mkdirs() && !directory.isDirectory())
49+
throw new IOException("Could not create directory: " + directory);
2750
}
2851

2952
public static void mkdirs(File outdir,String path)
3053
{
31-
File d = new File(outdir, path);
32-
if( !d.exists() )
33-
d.mkdirs();
54+
try
55+
{
56+
createDirectories(outdir, path);
57+
}
58+
catch (IOException e)
59+
{
60+
throw new IllegalArgumentException(e);
61+
}
3462
}
3563

3664
public static String dirpart(String name)
@@ -55,9 +83,11 @@ public static void extract(InputStream zipfile, File outdir)
5583
while ((entry = zin.getNextEntry()) != null)
5684
{
5785
name = entry.getName();
86+
// Validate every entry before creating directories or opening files.
87+
resolveEntry(outdir, name);
5888
if( entry.isDirectory() )
5989
{
60-
mkdirs(outdir,name);
90+
createDirectories(outdir,name);
6191
continue;
6292
}
6393
/* this part is necessary because file entry can come before
@@ -68,7 +98,7 @@ public static void extract(InputStream zipfile, File outdir)
6898
*/
6999
dir = dirpart(name);
70100
if( dir != null )
71-
mkdirs(outdir,dir);
101+
createDirectories(outdir,dir);
72102

73103
extractFile(zin, outdir, name);
74104
}

tests/android/OFZipUtil/run.sh

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
REPO_ROOT="$(git rev-parse --show-toplevel)"
5+
TEST_CLASSES="$(mktemp -d)"
6+
7+
javac -d "$TEST_CLASSES" \
8+
"$REPO_ROOT/tests/android/OFZipUtil/stubs/android/util/Log.java" \
9+
"$REPO_ROOT/tests/android/OFZipUtil/stubs/androidx/annotation/Keep.java" \
10+
"$REPO_ROOT/addons/ofxAndroid/Java/cc/openframeworks/OFZipUtil.java" \
11+
"$REPO_ROOT/tests/android/OFZipUtil/src/OFZipUtilTest.java"
12+
13+
java -cp "$TEST_CLASSES" OFZipUtilTest
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import cc.openframeworks.OFZipUtil;
2+
3+
import java.io.ByteArrayInputStream;
4+
import java.io.ByteArrayOutputStream;
5+
import java.io.File;
6+
import java.nio.charset.StandardCharsets;
7+
import java.nio.file.Files;
8+
import java.nio.file.Path;
9+
import java.util.zip.ZipEntry;
10+
import java.util.zip.ZipOutputStream;
11+
12+
public class OFZipUtilTest
13+
{
14+
private static byte[] zip(String name, String contents, boolean directory) throws Exception
15+
{
16+
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
17+
try (ZipOutputStream zip = new ZipOutputStream(bytes))
18+
{
19+
ZipEntry entry = new ZipEntry(name);
20+
zip.putNextEntry(entry);
21+
if (!directory)
22+
zip.write(contents.getBytes(StandardCharsets.UTF_8));
23+
zip.closeEntry();
24+
}
25+
return bytes.toByteArray();
26+
}
27+
28+
private static void extract(File output, String name, String contents) throws Exception
29+
{
30+
OFZipUtil.extract(new ByteArrayInputStream(zip(name, contents, false)), output);
31+
}
32+
33+
private static void assertMissing(Path path, String message)
34+
{
35+
if (Files.exists(path))
36+
throw new AssertionError(message + ": " + path);
37+
}
38+
39+
public static void main(String[] args) throws Exception
40+
{
41+
Path parent = Files.createTempDirectory("ofziputil-test-");
42+
File output = Files.createDirectory(parent.resolve("output")).toFile();
43+
44+
extract(output, "nested/good.txt", "good");
45+
Path good = output.toPath().resolve("nested/good.txt");
46+
if (!Files.isRegularFile(good)
47+
|| !"good".equals(Files.readString(good, StandardCharsets.UTF_8)))
48+
throw new AssertionError("A safe nested entry was not extracted correctly");
49+
50+
Path escaped = parent.resolve("escaped.txt");
51+
extract(output, "../escaped.txt", "bad");
52+
assertMissing(escaped, "A parent traversal entry escaped the output directory");
53+
54+
extract(output, "nested/../../escaped.txt", "bad");
55+
assertMissing(escaped, "A nested parent traversal entry escaped the output directory");
56+
57+
Path absolute = parent.resolve("absolute.txt");
58+
extract(output, absolute.toString(), "bad");
59+
assertMissing(absolute, "An absolute entry escaped the output directory");
60+
61+
Path escapedDirectory = parent.resolve("escaped-directory");
62+
OFZipUtil.extract(
63+
new ByteArrayInputStream(zip("../escaped-directory/", "", true)), output);
64+
assertMissing(escapedDirectory, "A directory entry escaped the output directory");
65+
66+
Path outside = Files.createDirectory(parent.resolve("outside"));
67+
Files.createSymbolicLink(output.toPath().resolve("link"), outside);
68+
extract(output, "link/escaped.txt", "bad");
69+
assertMissing(outside.resolve("escaped.txt"),
70+
"An entry escaped through a symbolic link in the output directory");
71+
72+
System.out.println("OFZipUtil security regression tests passed");
73+
}
74+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#ifndef TARGET_ANDROID
2+
int main() {
3+
return 0;
4+
}
5+
#endif
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package android.util;
2+
3+
public class Log
4+
{
5+
public static int i(String tag, String message)
6+
{
7+
return 0;
8+
}
9+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package androidx.annotation;
2+
3+
public @interface Keep
4+
{
5+
}

0 commit comments

Comments
 (0)