Skip to content

Commit 7bfffab

Browse files
mikehardytimrae
authored andcommitted
Add method verifying one path is inside another, with tests (#4860)
1 parent a1f2268 commit 7bfffab

File tree

3 files changed

+83
-0
lines changed

3 files changed

+83
-0
lines changed

AnkiDroid/src/main/java/com/ichi2/libanki/Utils.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import android.content.pm.ResolveInfo;
2727
import android.content.res.Resources;
2828
import android.net.Uri;
29+
import android.support.annotation.NonNull;
2930
import android.text.Html;
3031

3132
import com.ichi2.anki.AnkiFont;
@@ -631,6 +632,11 @@ public static void unzipFiles(ZipFile zipFile, String targetDirectory, String[]
631632
name = zipEntryToFilenameMap.get(name);
632633
}
633634
File destFile = new File(dir, name);
635+
if (!isInside(destFile, dir)) {
636+
Timber.e("Refusing to decompress invalid path: " + destFile.getCanonicalPath());
637+
throw new IOException("File is outside extraction target directory.");
638+
}
639+
634640
if (!ze.isDirectory()) {
635641
Timber.i("uncompress %s", name);
636642
zis = new BufferedInputStream(zipFile.getInputStream(ze));
@@ -655,6 +661,18 @@ public static void unzipFiles(ZipFile zipFile, String targetDirectory, String[]
655661
}
656662
}
657663

664+
/**
665+
* Checks to see if a given file path resides inside a given directory.
666+
* Useful for protection against path traversal attacks prior to creating the file
667+
* @param file the file with an uncertain filesystem location
668+
* @param dir the directory that should contain the file
669+
* @return true if the file path is inside the directory
670+
* @exception IOException if there are security or filesystem issues determining the paths
671+
*/
672+
public static boolean isInside(@NonNull File file, @NonNull File dir) throws IOException {
673+
return file.getCanonicalPath().startsWith(dir.getCanonicalPath());
674+
}
675+
658676
/**
659677
* Compress data.
660678
* @param bytesToCompress is the byte array to compress.
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package com.ichi2.libanki;
2+
3+
import junit.framework.Assert;
4+
5+
//import net.lachlanmckee.timberjunit.TimberTestRule;
6+
7+
//import org.junit.Rule;
8+
import org.junit.Test;
9+
10+
import java.io.File;
11+
import java.io.IOException;
12+
import java.net.URL;
13+
import java.util.Enumeration;
14+
import java.util.zip.ZipEntry;
15+
import java.util.zip.ZipFile;
16+
17+
public class UtilsTest {
18+
19+
// This depends on a Timber upgrade that should be pursued separately
20+
//@Rule
21+
//public TimberTestRule logAllAlwaysRule = TimberTestRule.logAllAlways();
22+
23+
@Test
24+
public void testZipWithPathTraversal() {
25+
26+
ClassLoader classLoader = getClass().getClassLoader();
27+
URL resource = classLoader.getResource("path-traversal.zip");
28+
File file = new File(resource.getPath());
29+
try {
30+
ZipFile zipFile = new ZipFile(file);
31+
Enumeration zipEntries = zipFile.entries();
32+
while (zipEntries.hasMoreElements()) {
33+
ZipEntry ze2 = (ZipEntry) zipEntries.nextElement();
34+
Utils.unzipFiles(zipFile, "/tmp", new String[]{ze2.getName()}, null);
35+
}
36+
Assert.fail("Expected an IOException");
37+
}
38+
catch (IOException e) {
39+
Assert.assertEquals(e.getMessage(), "File is outside extraction target directory.");
40+
}
41+
}
42+
43+
@Test
44+
public void testInvalidPaths() {
45+
try {
46+
File tmpDir = new File("/tmp");
47+
Assert.assertFalse(Utils.isInside(new File(tmpDir, "../foo"), tmpDir));
48+
Assert.assertFalse(Utils.isInside(new File(tmpDir, "/tmp/one/../../../foo"), tmpDir));
49+
} catch (IOException ioe) {
50+
Assert.fail("Unexpected exception: " + ioe);
51+
}
52+
}
53+
54+
@Test
55+
public void testValidPaths() {
56+
try {
57+
File tmpDir = new File("/tmp");
58+
Assert.assertTrue(Utils.isInside(new File(tmpDir, "test/file/path/no/parent"), tmpDir));
59+
Assert.assertTrue(Utils.isInside(new File(tmpDir, "/tmp/absolute/path"), tmpDir));
60+
Assert.assertTrue(Utils.isInside(new File(tmpDir, "test/file/../../"), tmpDir));
61+
} catch (IOException ioe) {
62+
Assert.fail("Unexpected exception: " + ioe);
63+
}
64+
}
65+
}
313 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)