Truyền tải qua đường dẫn Zip

Danh mục OWASP: MASVS-STORAGE: Bộ nhớ

Tổng quan

Lỗ hổng bảo mật Truyền tải qua đường dẫn Zip (còn gọi là ZipSlip) liên quan đến việc xử lý các tệp lưu trữ nén. Trên trang này, chúng tôi minh hoạ lỗ hổng bảo mật đó bằng cách sử dụng định dạng ZIP làm ví dụ, nhưng các vấn đề tương tự có thể phát sinh trong các thư viện xử lý các định dạng khác, như TAR, RAR hoặc 7z.

Nguyên nhân cơ bản của vấn đề này là do bên trong các tệp lưu trữ ZIP, mỗi tệp đóng gói được lưu trữ với tên đủ điều kiện, tên này cho phép các ký tự đặc biệt như dấu gạch chéo và dấu chấm. Thư viện mặc định trong gói java.util.zip không kiểm tra tên của các mục lưu trữ đối với các ký tự truyền tải thư mục (../). Vì vậy, bạn phải thật cẩn trọng khi liên kết tên được trích xuất từ kho lưu trữ bằng đường dẫn thư mục có mục tiêu.

Bạn cần phải xác thực mọi đoạn mã hoặc thư viện trích xuất ZIP từ các nguồn bên ngoài. Nhiều thư viện như vậy dễ bị quy trình Truyền tải đường dẫn Zip tấn công.

Tác động

Bạn có thể dùng lỗ hổng bảo mật Truyền tải qua đường dẫn Zip để ghi đè tệp tuỳ ý. Tuỳ thuộc vào điều kiện, tác động có thể khác nhau, nhưng trong nhiều trường hợp, lỗ hổng bảo mật này có thể dẫn đến các vấn đề bảo mật lớn như thực thi mã.

Giải pháp giảm thiểu

Để giảm thiểu vấn đề này, trước khi trích xuất từng mục nhập, bạn phải luôn xác minh rằng đường dẫn mục tiêu là con của thư mục đích. Đoạn mã dưới đây giả định rằng thư mục đích là an toàn – chỉ ứng dụng của bạn mới có thể ghi và thư mục không thuộc quyền kiểm soát của kẻ tấn công – nếu không, ứng dụng của bạn có thể dễ bị các lỗ hổng bảo mật khác như các cuộc tấn công liên kết tượng trưng.

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;
 }

Để tránh vô tình ghi đè tệp hiện có, bạn cũng phải đảm bảo rằng thư mục đích trống trước khi bắt đầu quá trình trích xuất. Nếu không, ứng dụng của bạn có nguy cơ gặp sự cố hoặc trong các trường hợp nghiêm trọng, ứng dụng sẽ bị xâm phạm.

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);
        …
    }
  }
}

Tài nguyên