From 3a67b5235f1364ac6d7d1bfe134cacfd54841d45 Mon Sep 17 00:00:00 2001 From: Raymond Lai Date: Thu, 26 Mar 2026 00:16:55 +0800 Subject: [PATCH 1/2] Fixes for path traversal vulnerabilities --- .../compressed/CompressedHelper.java | 7 ++- .../AbstractCommonsArchiveExtractor.kt | 5 +- .../helpers/SevenZipExtractor.kt | 3 + .../extractcontents/helpers/ZipExtractor.kt | 9 +-- .../extractcontents/helpers/RarExtractor.kt | 15 +++-- .../extractcontents/SevenZipExtractorTest.kt | 56 +++++++++++++++++ .../extractcontents/TarGzExtractorTest.kt | 57 ++++++++++++++++++ .../extractcontents/ZipExtractorTest.kt | 57 ++++++++++++++++++ app/src/test/resources/malicious.7z | Bin 0 -> 273 bytes app/src/test/resources/malicious.tar.gz | Bin 0 -> 223 bytes app/src/test/resources/malicious.zip | Bin 0 -> 419 bytes 11 files changed, 196 insertions(+), 13 deletions(-) create mode 100644 app/src/test/resources/malicious.7z create mode 100644 app/src/test/resources/malicious.tar.gz create mode 100644 app/src/test/resources/malicious.zip diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/compressed/CompressedHelper.java b/app/src/main/java/com/amaze/filemanager/filesystem/compressed/CompressedHelper.java index 5d36ad0875..ec08a49d76 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/compressed/CompressedHelper.java +++ b/app/src/main/java/com/amaze/filemanager/filesystem/compressed/CompressedHelper.java @@ -228,8 +228,11 @@ public static String getFileName(String compressedName) { } } - public static final boolean isEntryPathValid(String entryPath) { - return !entryPath.startsWith("..\\") && !entryPath.startsWith("../") && !entryPath.equals(".."); + public static boolean isEntryPathValid(String entryPath) { + return !entryPath.startsWith("..\\") + && !entryPath.startsWith("../") + && !entryPath.equals("..") + && !entryPath.contains("/../"); } private static boolean isZip(String type) { diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/compressed/extractcontents/helpers/AbstractCommonsArchiveExtractor.kt b/app/src/main/java/com/amaze/filemanager/filesystem/compressed/extractcontents/helpers/AbstractCommonsArchiveExtractor.kt index d9e8e63899..0a1207aad8 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/compressed/extractcontents/helpers/AbstractCommonsArchiveExtractor.kt +++ b/app/src/main/java/com/amaze/filemanager/filesystem/compressed/extractcontents/helpers/AbstractCommonsArchiveExtractor.kt @@ -67,7 +67,7 @@ abstract class AbstractCommonsArchiveExtractor( } } } - if (archiveEntries.size > 0) { + if (archiveEntries.isNotEmpty()) { listener.onStart(totalBytes, archiveEntries[0].name) inputStream.close() inputStream = createFrom(FileInputStream(filePath)) @@ -101,6 +101,9 @@ abstract class AbstractCommonsArchiveExtractor( return } val outputFile = File(outputDir, entry.name) + if (!outputFile.canonicalPath.startsWith(File(outputDir).canonicalPath + File.separator)) { + throw IOException("Incorrect archive entry path: ${entry.name}") + } if (false == outputFile.parentFile?.exists()) { MakeDirectoryOperation.mkdir(outputFile.parentFile, context) } diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/compressed/extractcontents/helpers/SevenZipExtractor.kt b/app/src/main/java/com/amaze/filemanager/filesystem/compressed/extractcontents/helpers/SevenZipExtractor.kt index b39c9db054..88d27b729d 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/compressed/extractcontents/helpers/SevenZipExtractor.kt +++ b/app/src/main/java/com/amaze/filemanager/filesystem/compressed/extractcontents/helpers/SevenZipExtractor.kt @@ -111,6 +111,9 @@ class SevenZipExtractor( return } val outputFile = File(outputDir, name) + if (!outputFile.canonicalPath.startsWith(File(outputDir).canonicalPath + File.separator)) { + throw IOException("Incorrect 7z entry path: $name") + } if (!outputFile.parentFile.exists()) { MakeDirectoryOperation.mkdir(outputFile.parentFile, context) } diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/compressed/extractcontents/helpers/ZipExtractor.kt b/app/src/main/java/com/amaze/filemanager/filesystem/compressed/extractcontents/helpers/ZipExtractor.kt index 368bbbf2b3..313ff808a5 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/compressed/extractcontents/helpers/ZipExtractor.kt +++ b/app/src/main/java/com/amaze/filemanager/filesystem/compressed/extractcontents/helpers/ZipExtractor.kt @@ -21,7 +21,6 @@ package com.amaze.filemanager.filesystem.compressed.extractcontents.helpers import android.content.Context -import android.os.Build import com.amaze.filemanager.R import com.amaze.filemanager.application.AppConfig import com.amaze.filemanager.fileoperations.filesystem.compressed.ArchivePasswordCache @@ -47,8 +46,6 @@ class ZipExtractor( listener: OnUpdate, updatePosition: UpdatePosition, ) : Extractor(context, filePath, outputPath, listener, updatePosition) { - private val isRobolectricTest = Build.HARDWARE == "robolectric" - @Throws(IOException::class) override fun extractWithFilter(filter: Filter) { var totalBytes: Long = 0 @@ -110,9 +107,9 @@ class ZipExtractor( outputDir: String, ) { val outputFile = File(outputDir, fixEntryName(entry.fileName)) - if (!outputFile.canonicalPath.startsWith(outputDir) && - (isRobolectricTest && !outputFile.canonicalPath.startsWith("/private$outputDir")) - ) { + val canonicalOutput = outputFile.canonicalPath + val canonicalDir = File(outputDir).canonicalPath + File.separator + if (!canonicalOutput.startsWith(canonicalDir)) { throw IOException("Incorrect ZipEntry path!") } if (entry.isDirectory) { diff --git a/app/src/play/java/com/amaze/filemanager/filesystem/compressed/extractcontents/helpers/RarExtractor.kt b/app/src/play/java/com/amaze/filemanager/filesystem/compressed/extractcontents/helpers/RarExtractor.kt index 672298019f..b50de82d80 100644 --- a/app/src/play/java/com/amaze/filemanager/filesystem/compressed/extractcontents/helpers/RarExtractor.kt +++ b/app/src/play/java/com/amaze/filemanager/filesystem/compressed/extractcontents/helpers/RarExtractor.kt @@ -77,9 +77,11 @@ class RarExtractor( MainHeaderNullException::class.java.isAssignableFrom(it::class.java) -> { throw BadArchiveNotice(it) } + UnsupportedRarV5Exception::class.java.isAssignableFrom(it::class.java) -> { throw it } + else -> { throw PasswordRequiredException(filePath) } @@ -144,9 +146,9 @@ class RarExtractor( CompressedHelper.SEPARATOR, ) val outputFile = File(outputDir, name) - if (!outputFile.canonicalPath.startsWith(outputDir) && - (isRobolectricTest && !outputFile.canonicalPath.startsWith("/private$outputDir")) - ) { + val canonicalOutput = outputFile.canonicalPath + val canonicalDir = File(outputDir).canonicalPath + File.separator + if (!canonicalOutput.startsWith(canonicalDir)) { throw IOException("Incorrect RAR FileHeader path!") } if (entry.isDirectory) { @@ -232,7 +234,12 @@ class RarExtractor( "\\\\".toRegex(), CompressedHelper.SEPARATOR, ) - extractEntry(context, archive, header, context.externalCacheDir!!.absolutePath) + extractEntry( + context, + archive, + header, + context.externalCacheDir!!.absolutePath, + ) return "${context.externalCacheDir!!.absolutePath}/$filename" } } diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/compressed/extractcontents/SevenZipExtractorTest.kt b/app/src/test/java/com/amaze/filemanager/filesystem/compressed/extractcontents/SevenZipExtractorTest.kt index d730b39644..2b6c7b7703 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/compressed/extractcontents/SevenZipExtractorTest.kt +++ b/app/src/test/java/com/amaze/filemanager/filesystem/compressed/extractcontents/SevenZipExtractorTest.kt @@ -20,10 +20,66 @@ package com.amaze.filemanager.filesystem.compressed.extractcontents +import android.os.Environment +import androidx.test.core.app.ApplicationProvider +import com.amaze.filemanager.asynchronous.management.ServiceWatcherUtil import com.amaze.filemanager.filesystem.compressed.extractcontents.helpers.SevenZipExtractor +import org.junit.Assert.assertFalse +import org.junit.Assert.fail +import org.junit.Test +import java.io.File +import java.io.IOException open class SevenZipExtractorTest : AbstractArchiveExtractorTest() { override val archiveType: String = "7z" override fun extractorClass(): Class = SevenZipExtractor::class.java + + /** + * Verify that a 7-Zip archive carrying a path-traversal entry + * (../POC_7Z_PROOF.txt) is blocked by the canonical-path guard: + * - extractEverything() must throw IOException + * - no file is written outside the designated output directory + */ + @Test + fun testExtractMalicious7z() { + val maliciousArchive = File(Environment.getExternalStorageDirectory(), "malicious.7z") + val outputDir = Environment.getExternalStorageDirectory() + val extractor = + SevenZipExtractor( + ApplicationProvider.getApplicationContext(), + maliciousArchive.absolutePath, + outputDir.absolutePath, + object : Extractor.OnUpdate { + override fun onStart( + totalBytes: Long, + firstEntryName: String, + ) = Unit + + override fun onUpdate(entryPath: String) = Unit + + override fun isCancelled(): Boolean = false + + override fun onFinish() = Unit + }, + ServiceWatcherUtil.UPDATE_POSITION, + ) + + try { + extractor.extractEverything() + fail("Expected IOException: canonical-path guard must reject the traversal entry") + } catch (e: IOException) { + // Confirm the guard fired (not a generic bad-archive error) + assertFalse( + "Exception must not be a BadArchiveNotice", + e is Extractor.BadArchiveNotice, + ) + } + + // The malicious file must NOT have been written outside the output directory + assertFalse( + "Malicious file must not escape the output directory", + File(outputDir.parentFile, "POC_7Z_PROOF.txt").exists(), + ) + } } diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/compressed/extractcontents/TarGzExtractorTest.kt b/app/src/test/java/com/amaze/filemanager/filesystem/compressed/extractcontents/TarGzExtractorTest.kt index 0f537554b4..bcf9805724 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/compressed/extractcontents/TarGzExtractorTest.kt +++ b/app/src/test/java/com/amaze/filemanager/filesystem/compressed/extractcontents/TarGzExtractorTest.kt @@ -20,10 +20,67 @@ package com.amaze.filemanager.filesystem.compressed.extractcontents +import android.os.Environment +import androidx.test.core.app.ApplicationProvider +import com.amaze.filemanager.asynchronous.management.ServiceWatcherUtil import com.amaze.filemanager.filesystem.compressed.extractcontents.helpers.TarGzExtractor +import org.junit.Assert.assertFalse +import org.junit.Assert.fail +import org.junit.Test +import java.io.File +import java.io.IOException open class TarGzExtractorTest : AbstractArchiveExtractorTest() { override val archiveType: String = "tar.gz" override fun extractorClass(): Class = TarGzExtractor::class.java + + /** + * Verify that a tar.gz archive carrying a path-traversal entry + * (../POC_ZIPSLIP_PROOF.txt) is blocked by the canonical-path guard + * in AbstractCommonsArchiveExtractor: + * - extractEverything() must throw IOException + * - no file is written outside the designated output directory + */ + @Test + fun testExtractMaliciousTarGz() { + val maliciousArchive = File(Environment.getExternalStorageDirectory(), "malicious.tar.gz") + val outputDir = Environment.getExternalStorageDirectory() + val extractor = + TarGzExtractor( + ApplicationProvider.getApplicationContext(), + maliciousArchive.absolutePath, + outputDir.absolutePath, + object : Extractor.OnUpdate { + override fun onStart( + totalBytes: Long, + firstEntryName: String, + ) = Unit + + override fun onUpdate(entryPath: String) = Unit + + override fun isCancelled(): Boolean = false + + override fun onFinish() = Unit + }, + ServiceWatcherUtil.UPDATE_POSITION, + ) + + try { + extractor.extractEverything() + fail("Expected IOException: canonical-path guard must reject the traversal entry") + } catch (e: IOException) { + // Confirm the guard fired (not a generic bad-archive error) + assertFalse( + "Exception must not be a BadArchiveNotice", + e is Extractor.BadArchiveNotice, + ) + } + + // The malicious file must NOT have been written outside the output directory + assertFalse( + "Malicious file must not escape the output directory", + File(outputDir.parentFile, "POC_ZIPSLIP_PROOF.txt").exists(), + ) + } } diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/compressed/extractcontents/ZipExtractorTest.kt b/app/src/test/java/com/amaze/filemanager/filesystem/compressed/extractcontents/ZipExtractorTest.kt index 418868998e..2e66ee0396 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/compressed/extractcontents/ZipExtractorTest.kt +++ b/app/src/test/java/com/amaze/filemanager/filesystem/compressed/extractcontents/ZipExtractorTest.kt @@ -20,10 +20,67 @@ package com.amaze.filemanager.filesystem.compressed.extractcontents +import android.os.Environment +import androidx.test.core.app.ApplicationProvider +import com.amaze.filemanager.asynchronous.management.ServiceWatcherUtil import com.amaze.filemanager.filesystem.compressed.extractcontents.helpers.ZipExtractor +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import java.io.File class ZipExtractorTest : AbstractArchiveExtractorTest() { override val archiveType: String = "zip" override fun extractorClass(): Class = ZipExtractor::class.java + + /** + * Verify that a ZIP archive carrying a path-traversal entry + * (foo/../../POC_ZIP_PROOF.txt) is handled safely: + * - extraction completes without an exception + * - the offending entry is recorded in invalidArchiveEntries + * - no file is written outside the designated output directory + */ + @Test + fun testExtractMaliciousZip() { + val maliciousArchive = File(Environment.getExternalStorageDirectory(), "malicious.zip") + val outputDir = Environment.getExternalStorageDirectory() + val extractor = + ZipExtractor( + ApplicationProvider.getApplicationContext(), + maliciousArchive.absolutePath, + outputDir.absolutePath, + object : Extractor.OnUpdate { + override fun onStart( + totalBytes: Long, + firstEntryName: String, + ) = Unit + + override fun onUpdate(entryPath: String) = Unit + + override fun isCancelled(): Boolean = false + + override fun onFinish() = Unit + }, + ServiceWatcherUtil.UPDATE_POSITION, + ) + + // Extraction must succeed — path-traversal entries are quarantined, not thrown + extractor.extractEverything() + + // The traversal entry must be recorded as invalid … + assertTrue( + "Malicious path-traversal entry must be captured in invalidArchiveEntries", + extractor.invalidArchiveEntries.isNotEmpty(), + ) + assertTrue( + "invalidArchiveEntries must contain the POC entry", + extractor.invalidArchiveEntries.any { "POC_ZIP_PROOF" in it }, + ) + // … and must NOT have been written outside the output directory + assertFalse( + "Malicious file must not escape the output directory", + File(outputDir.parentFile, "POC_ZIP_PROOF.txt").exists(), + ) + } } diff --git a/app/src/test/resources/malicious.7z b/app/src/test/resources/malicious.7z new file mode 100644 index 0000000000000000000000000000000000000000..5576b92418b2920be2945de38e1003ebe801fba5 GIT binary patch literal 273 zcmXr7+Ou9=hJoepyw$gFGeCeCls2uOZT*13hruJ3L0$Tk+q+vEJhiTUTk~lBhl+jf z_PpyaOuj08b*QN{PiwE{yG2X?6_>T! z{55o7V0gf=fT1~-fuYfueI5U$gZY0Qc27x>se7H2{B7s-Nm{oT?qBn42A}7>f|Hz* zcgwou>Tm9S6Czi>`S&vMrKO2!EKgN7e2lU&&X0>fIBDv;AGcJ0yZ6aC26Hsb@0{N8 zUnrGHqFVRY`;dOa6#3lE&bo1n&rEJ@W?#mqI4{=la{j@ubJ^B@zP1tQ5C#Tuwg5)X VhHeISZbk-1MMXvlo`!`C3;=Qm{=Z?pclAB}O;PJ6IO!@t`;*jeYqjjHwylJs6z40wb-|Y6!{wETR1gbI)y$|7@bSO8@}SQ@HAPfs6+0{orhqdWuR1A_eh z-9Yw*WMmdYY%5PJRwysZECE`nkY8F-oSBlUP?C|VkXlhvl$czSnV+YSl3A3RT#{c@ z333u6lL#|zZvkxug9b(rh2}|Q&DeYd(JH|3-_Z%kg!m8DI&>dGbTBY9Fs49t;P7vN SH!B-RF*6YE0Mb`M90mX)e^%E3 literal 0 HcmV?d00001 From 7d970e953784d6306a6ecb824b358f0ba05af8cb Mon Sep 17 00:00:00 2001 From: Raymond Lai Date: Sun, 29 Mar 2026 10:11:09 +0800 Subject: [PATCH 2/2] Changes per PR feedback --- .../filesystem/compressed/CompressedHelper.java | 17 +++++++++++++---- .../helpers/AbstractCommonsArchiveExtractor.kt | 10 ++++++---- .../helpers/SevenZipExtractor.kt | 13 +++++++------ .../extractcontents/helpers/ZipExtractor.kt | 5 ++++- .../extractcontents/ZipExtractorTest.kt | 7 ++++++- 5 files changed, 36 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/compressed/CompressedHelper.java b/app/src/main/java/com/amaze/filemanager/filesystem/compressed/CompressedHelper.java index ec08a49d76..26a4c6ab55 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/compressed/CompressedHelper.java +++ b/app/src/main/java/com/amaze/filemanager/filesystem/compressed/CompressedHelper.java @@ -229,10 +229,19 @@ public static String getFileName(String compressedName) { } public static boolean isEntryPathValid(String entryPath) { - return !entryPath.startsWith("..\\") - && !entryPath.startsWith("../") - && !entryPath.equals("..") - && !entryPath.contains("/../"); + if (entryPath == null || entryPath.isEmpty()) { + return false; + } + // Normalize path separators to handle both Unix and Windows-style paths. + String normalized = entryPath.replace('\\', '/'); + // Reject any path that attempts to traverse up the directory tree. + String[] segments = normalized.split("/"); + for (String segment : segments) { + if ("..".equals(segment)) { + return false; + } + } + return true; } private static boolean isZip(String type) { diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/compressed/extractcontents/helpers/AbstractCommonsArchiveExtractor.kt b/app/src/main/java/com/amaze/filemanager/filesystem/compressed/extractcontents/helpers/AbstractCommonsArchiveExtractor.kt index 0a1207aad8..4db930d239 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/compressed/extractcontents/helpers/AbstractCommonsArchiveExtractor.kt +++ b/app/src/main/java/com/amaze/filemanager/filesystem/compressed/extractcontents/helpers/AbstractCommonsArchiveExtractor.kt @@ -96,14 +96,16 @@ abstract class AbstractCommonsArchiveExtractor( entry: ArchiveEntry, outputDir: String, ) { + val outputFile = File(outputDir, entry.name) + if (!outputFile.canonicalPath.startsWith(File(outputDir).canonicalPath + File.separator) && + outputFile.canonicalPath != File(outputDir).canonicalPath + ) { + throw IOException("Incorrect archive entry path: ${entry.name}") + } if (entry.isDirectory) { MakeDirectoryOperation.mkdir(File(outputDir, entry.name), context) return } - val outputFile = File(outputDir, entry.name) - if (!outputFile.canonicalPath.startsWith(File(outputDir).canonicalPath + File.separator)) { - throw IOException("Incorrect archive entry path: ${entry.name}") - } if (false == outputFile.parentFile?.exists()) { MakeDirectoryOperation.mkdir(outputFile.parentFile, context) } diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/compressed/extractcontents/helpers/SevenZipExtractor.kt b/app/src/main/java/com/amaze/filemanager/filesystem/compressed/extractcontents/helpers/SevenZipExtractor.kt index 88d27b729d..84c656f4b5 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/compressed/extractcontents/helpers/SevenZipExtractor.kt +++ b/app/src/main/java/com/amaze/filemanager/filesystem/compressed/extractcontents/helpers/SevenZipExtractor.kt @@ -105,15 +105,16 @@ class SevenZipExtractor( entry: SevenZArchiveEntry, outputDir: String, ) { - val name = entry.name + val outputFile = File(outputDir, entry.name) + if (!outputFile.canonicalPath.startsWith(File(outputDir).canonicalPath + File.separator) && + outputFile.canonicalPath != File(outputDir).canonicalPath + ) { + throw IOException("Incorrect archive entry path: ${entry.name}") + } if (entry.isDirectory) { - MakeDirectoryOperation.mkdir(File(outputDir, name), context) + MakeDirectoryOperation.mkdir(File(outputDir, entry.name), context) return } - val outputFile = File(outputDir, name) - if (!outputFile.canonicalPath.startsWith(File(outputDir).canonicalPath + File.separator)) { - throw IOException("Incorrect 7z entry path: $name") - } if (!outputFile.parentFile.exists()) { MakeDirectoryOperation.mkdir(outputFile.parentFile, context) } diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/compressed/extractcontents/helpers/ZipExtractor.kt b/app/src/main/java/com/amaze/filemanager/filesystem/compressed/extractcontents/helpers/ZipExtractor.kt index 313ff808a5..6a240fabe9 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/compressed/extractcontents/helpers/ZipExtractor.kt +++ b/app/src/main/java/com/amaze/filemanager/filesystem/compressed/extractcontents/helpers/ZipExtractor.kt @@ -110,7 +110,10 @@ class ZipExtractor( val canonicalOutput = outputFile.canonicalPath val canonicalDir = File(outputDir).canonicalPath + File.separator if (!canonicalOutput.startsWith(canonicalDir)) { - throw IOException("Incorrect ZipEntry path!") + throw IOException( + "Refusing to extract Zip entry '${entry.fileName}' to '$canonicalOutput' " + + "outside target directory '$canonicalDir'", + ) } if (entry.isDirectory) { // zip entry is a directory, return after creating new directory diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/compressed/extractcontents/ZipExtractorTest.kt b/app/src/test/java/com/amaze/filemanager/filesystem/compressed/extractcontents/ZipExtractorTest.kt index 2e66ee0396..89f2ea2237 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/compressed/extractcontents/ZipExtractorTest.kt +++ b/app/src/test/java/com/amaze/filemanager/filesystem/compressed/extractcontents/ZipExtractorTest.kt @@ -78,9 +78,14 @@ class ZipExtractorTest : AbstractArchiveExtractorTest() { extractor.invalidArchiveEntries.any { "POC_ZIP_PROOF" in it }, ) // … and must NOT have been written outside the output directory + val escapedFile = File(outputDir, "foo/../../POC_ZIP_PROOF.txt").canonicalFile assertFalse( "Malicious file must not escape the output directory", - File(outputDir.parentFile, "POC_ZIP_PROOF.txt").exists(), + escapedFile.exists(), + ) + assertFalse( + "Escaped file canonical path must not reside under output directory", + escapedFile.canonicalPath.startsWith(outputDir.canonicalPath), ) } }