مسح مسار ملفات Zip

فئة OWASP: MASVS-STORAGE: مساحة التخزين

نظرة عامة

تتعلّق ثغرة Zip Path Traversal الأمنية، المعروفة أيضًا باسم ZipSlip، بالتعامل مع الأرشيفات المضغوطة. في هذه الصفحة، نوضّح هذه الثغرة الأمنية باستخدام تنسيق ZIP كمثال، ولكن يمكن أن تنشأ مشاكل مشابهة في المكتبات التي تتعامل مع تنسيقات أخرى، مثل TAR أو RAR أو 7z.

السبب الأساسي لهذه المشكلة هو أنّه داخل أرشيفات ZIP، يتم تخزين كل ملف مضغوط باسم مؤهَّل بالكامل، ما يسمح باستخدام رموز خاصة مثل الشرطات المائلة والنقاط. لا تتحقّق المكتبة التلقائية من حزمة java.util.zip من أسماء إدخالات الأرشيف بحثًا عن أحرف مسح الدليل (../)، لذا يجب توخّي الحذر عند ربط الاسم المستخرَج من الأرشيف بمسار الدليل المستهدَف.

من المهم جدًا التحقّق من صحة أي مقتطفات رمز أو مكتبات لاستخراج ملفات ZIP من مصادر خارجية. العديد من هذه المكتبات معرَّضة لثغرات مسح مسار ملفات Zip.

التأثير

يمكن استخدام ثغرة مسح مسار ملفات Zip لتحقيق الكتابة فوق الملفات العشوائية. واستنادًا إلى الظروف، قد يختلف التأثير، ولكن في العديد من الحالات، يمكن أن يؤدي هذا الثغرة الأمنية إلى مشاكل أمنية كبيرة، مثل تنفيذ التعليمات البرمجية.

إجراءات التخفيف

للتخفيف من هذه المشكلة، يجب التأكّد دائمًا من أنّ المسار المستهدف هو عنصر فرعي من دليل الوجهة قبل استخراج كل إدخال. يفترض الرمز البرمجي أدناه أنّ دليل الوجهة آمن، أي أنّ تطبيقك وحده يمكنه الكتابة فيه وليس تحت سيطرة المهاجم، وإلا قد يكون تطبيقك عرضة لثغرات أمنية أخرى، مثل هجمات الروابط الرمزية.

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