Zip 路徑遍歷

OWASP 類別:MASVS-STORAGE:儲存空間

總覽

Zip Path Traversal 安全漏洞 (又稱為 ZipSlip) 與處理壓縮封存的程序有關。在這個頁面中,我們將以 ZIP 格式為例介紹這個安全漏洞,但在處理其他格式 (例如 TAR、RAR 或 7z) 的程式庫中,也可能會發生類似問題。

這個問題的根本原因是,在 ZIP 封存檔中,每個封裝檔案都會以完整名稱儲存,也就是允許使用斜線和點等特殊字元。java.util.zip 套件的預設程式庫不會檢查目錄周遊字元 (../) 的封存項目名稱,因此如果要將封存檔中擷取的名稱與目標目錄路徑串連在一起,必須格外留意。

請務必驗證來自外部來源的任何 ZIP 擷取程式碼片段或程式庫。這類程式庫有很多都存在 Zip Path Traversal 安全漏洞。

影響

Zip Path Traversal 安全漏洞可用於覆寫任意檔案。視條件而定,影響範圍可能會有所不同,但在許多情況下,這個安全漏洞可能會導致程式碼執行等重大安全性問題。

因應措施

為緩解此問題的影響,在擷取每個項目之前,您一律應驗證目標路徑是目的地目錄的子項。以下程式碼假設目的地目錄安全無虞 (只能由您的應用程式寫入,而不會遭到攻擊者控管),否則應用程式可能會受到其他安全漏洞攻擊,例如符號連結攻擊。

Kotlin

companion object {
    @Throws(IOException::class)
    fun newFile(targetPath: File, zipEntry: ZipEntry): File {
        val name: String = zipEntry.name
        val f = File(targetPath, name)
        val canonicalPath = f.canonicalPath
        if (!canonicalPath.startsWith(
                targetPath.canonicalPath + File.separator)) {
            throw ZipException("Illegal name: $name")
        }
        return f
    }
}

Java

public static File newFile(File targetPath, ZipEntry zipEntry) throws IOException {
    String name = zipEntry.getName();
    File f = new File(targetPath, name);
    String canonicalPath = f.getCanonicalPath();
    if (!canonicalPath.startsWith(targetPath.getCanonicalPath() + File.separator)) {
      throw new ZipException("Illegal name: " + name);
    }
    return f;
 }

為避免意外覆寫現有檔案,您也應確保目的地目錄為空白目錄,然後再啟動擷取程序。否則,應用程式可能會有當機風險,在極端情況下也可能會遭駭。

Kotlin

@Throws(IOException::class)
fun unzip(inputStream: InputStream?, destinationDir: File) {
    if (!destinationDir.isDirectory) {
        throw IOException("Destination is not a directory.")
    }
    val files = destinationDir.list()
    if (files != null && files.isNotEmpty()) {
        throw IOException("Destination directory is not empty.")
    }
    ZipInputStream(inputStream).use { zipInputStream ->
        var zipEntry: ZipEntry
        while (zipInputStream.nextEntry.also { zipEntry = it } != null) {
            val targetFile = File(destinationDir, zipEntry.name)
            // ...
        }
    }
}

Java

void unzip(final InputStream inputStream, File destinationDir)
      throws IOException {
  if(!destinationDir.isDirectory()) {
    throw IOException("Destination is not a directory.");
  }

  String[] files = destinationDir.list();
  if(files != null && files.length != 0) {
    throw IOException("Destination directory is not empty.");
  }

  try (ZipInputStream zipInputStream = new ZipInputStream(inputStream)) {
    ZipEntry zipEntry;
    while ((zipEntry = zipInputStream.getNextEntry()) != null) {
      final File targetFile = new File(destinationDir, zipEntry);
        …
    }
  }
}

資源

  • 注意:系統會在 JavaScript 關閉時顯示連結文字
  • 路徑遍歷