Zip Path Traversal

หมวดหมู่ OWASP: MASVS-STORAGE: Storage

ภาพรวม

ช่องโหว่ Zip Path Traversal หรือที่เรียกว่า ZipSlip เกี่ยวข้องกับการจัดการไฟล์ที่บีบอัด ในหน้านี้ เราจะแสดงช่องโหว่นี้โดยใช้รูปแบบ ZIP เป็นตัวอย่าง แต่ปัญหาที่คล้ายกันอาจเกิดขึ้นในไลบรารีที่จัดการรูปแบบอื่นๆ เช่น TAR, RAR หรือ 7z

เหตุผลพื้นฐานของปัญหานี้คือในไฟล์ ZIP ที่บีบอัด แต่ละไฟล์ที่แพ็กไว้จะจัดเก็บด้วยชื่อที่ระบุแบบสมบูรณ์ในตัวเอง ซึ่งอนุญาตให้ใช้สัญลักษณ์พิเศษ เช่น เครื่องหมายทับและจุด ไลบรารีเริ่มต้นจากแพ็กเกจ java.util.zip ไม่ได้ตรวจสอบชื่อของรายการที่บีบอัดเพื่อหาอักขระการข้ามไดเรกทอรี (../) ดังนั้นจึงต้องใช้ความระมัดระวังเป็นพิเศษเมื่อเชื่อมชื่อที่แยกออกมาจากไฟล์ที่บีบอัดกับเส้นทางไดเรกทอรีเป้าหมาย

การตรวจสอบข้อมูลโค้ดหรือไลบรารีที่แยกไฟล์ ZIP จากแหล่งที่มาภายนอกเป็นสิ่งสำคัญอย่างยิ่ง ไลบรารีจำนวนมากมีช่องโหว่ต่อการเกิด Zip Path Traversal

ผลกระทบ

ช่องโหว่ Zip Path Traversal อาจใช้เพื่อเขียนทับไฟล์ใดก็ได้ ผลกระทบอาจแตกต่างกันไปตามเงื่อนไข แต่ในหลายกรณี ช่องโหว่นี้อาจนำไปสู่ปัญหาด้านความปลอดภัยที่สำคัญ เช่น การดำเนินการโค้ด

การบรรเทาผลกระทบ

หากต้องการบรรเทาปัญหานี้ คุณควรตรวจสอบเสมอว่าเส้นทางเป้าหมายเป็นไดเรกทอรีย่อยของไดเรกทอรีปลายทางก่อนที่จะแยกแต่ละรายการ โค้ดด้านล่างนี้ถือว่าไดเรกทอรีปลายทางปลอดภัย ซึ่งแอปของคุณเขียนได้เท่านั้นและไม่ได้อยู่ภายใต้การควบคุมของผู้โจมตี มิฉะนั้นแอปของคุณอาจมีช่องโหว่อื่นๆ เช่น การโจมตีด้วย Symlink

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 ปิดอยู่
  • Path Traversal