התקפת Path Traversal בקובץ ZIP

קטגוריית OWASP: MASVS-STORAGE: אחסון

סקירה כללית

פרצת האבטחה מסוג Zip Path Traversal, שנקראת גם ZipSlip, קשורה לטיפול בארכיונים דחוסים. בדף הזה אנחנו מדגימים את הפגיעות הזו באמצעות פורמט ZIP כדוגמה, אבל בעיות דומות יכולות להתרחש בספריות שמטפלות בפורמטים אחרים, כמו TAR,‏ RAR או 7z.

הסיבה הבסיסית לבעיה הזו היא שבתוך ארכיוני ZIP, כל קובץ דחוס מאוחסן עם שם מלא, שמאפשר שימוש בתווים מיוחדים כמו לוכסנים ונקודות. ספריית ברירת המחדל מחבילת java.util.zip לא בודקת את השמות של רשומות הארכיון לגבי תווים של מעבר בין ספריות (../), ולכן צריך לנקוט משנה זהירות כשמשרשרים את השם שחולץ מהארכיון עם נתיב הספרייה המיועד.

חשוב מאוד לאמת קטעי קוד או ספריות לחילוץ קובצי ZIP ממקורות חיצוניים. הרבה ספריות כאלה פגיעות לפרצות אבטחה מסוג Zip Path Traversals.

השפעה

אפשר לנצל את פרצת האבטחה מסוג 'Path Traversal בקובץ 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);
        
    }
  }
}

משאבים