コンテンツにスキップ

ZIP Vulnerabilities: Path Traversal, Zip Symbolic Link, and Zip Extension Spoofing

ZIPファイルの脆弱性: パストラバーサル、ZIPシンボリックリンク、ZIP拡張子スプーフィング

概要

複数のファイルを保存・転送するための圧縮アーカイブであるZIPファイルは、利便性と様々なプラットフォーム間での互換性の高さから、広く採用されているファイル形式です。しかし、他のデジタルファイル形式と同様に、ZIPファイルも脆弱性と無縁ではありません。ZIPファイルに関連する一般的な脆弱性は以下の通りです。

パストラバーサル (Path Traversal)

ディレクトリトラバーサルとも呼ばれるパストラバーサルは、攻撃者が意図された展開先ディレクトリの外部にあるファイルやディレクトリにアクセスすることを可能にする脆弱性です。ZIPファイルを展開する際、展開プロセスがアーカイブ内のファイルパスを適切に検証しない場合、攻撃者はディレクトリをトラバースしてシステム上の機密ファイルにアクセスできるようにする特殊文字やシーケンスを含む悪意のあるZIPファイルを作成することができます。これにより、機密情報の不正な開示や、場合によってはリモートコードの実行につながる可能性があります。

import 'dart:io';
import 'archive/archive.dart';

void extractZipFile(String path) {
  File file = File(path);
  Archive archive = ZipDecoder().decodeBytes(file.readAsBytesSync());

  for (ArchiveFile archiveFile in archive) {
    // Insecure: Does not properly validate file paths
    File extractedFile = File('/tmp/' + archiveFile.name);
    extractedFile.createSync(recursive: true);
    extractedFile.writeAsBytesSync(archiveFile.content);
  }
}
import Foundation
import ZIPFoundation

func extractZipFile(path: String) {
    guard let archive = Archive(url: URL(fileURLWithPath: path), accessMode: .read) else {
        return
    }

    for entry in archive {
        // Insecure: Does not properly validate file paths
        let extractedFilePath = "/tmp/\(entry.path)"
        let extractedFileURL = URL(fileURLWithPath: extractedFilePath)

        do {
            try FileManager.default.createDirectory(atPath: extractedFileURL.deletingLastPathComponent().path,
                                                    withIntermediateDirectories: true,
                                                    attributes: nil)
            try archive.extract(entry, to: extractedFileURL)
        } catch {
            print("Extraction failed: \(error.localizedDescription)")
        }
    }
}
import java.io.File
import java.util.zip.ZipInputStream

fun extractZipFile(path: String) {
    val file = File(path)
    val zipInputStream = ZipInputStream(file.inputStream())

    var entry = zipInputStream.nextEntry
    while (entry != null) {
        // Insecure: Does not properly validate file paths
        val extractedFile = File("/tmp/" + entry.name)
        extractedFile.parentFile.mkdirs()
        extractedFile.outputStream().use { output ->
            zipInputStream.copyTo(output)
        }

        entry = zipInputStream.nextEntry
    }

    zipInputStream.close()
}

ZIPシンボリックリンク

シンボリックリンク(symlink)は、ショートカットや参照を作成するために使用されるファイルやディレクトリへのポインタです。しかし、ZIPファイルの展開プロセスがシンボリックリンクを適切に処理しない場合、攻撃者はターゲットシステム上の機密ファイルやディレクトリを指すシンボリックリンクを含む悪意のあるZIPファイルを作成することができます。展開時にこれらのシンボリックリンクが追跡されると、重要なファイルやディレクトリへの不正アクセスにつながる可能性があります。

import 'dart:io';
import 'archive/archive.dart';

void extractZipFile(String path) {
  File file = File(path);
  Archive archive = ZipDecoder().decodeBytes(file.readAsBytesSync());

  for (ArchiveFile archiveFile in archive) {
    // Insecure: Does not handle symbolic links properly
    if (archiveFile.isSymbolicLink) {
      File symlink = File('/tmp/' + archiveFile.name);
      symlink.createSync(recursive: true);
      symlink.writeAsStringSync(archiveFile.content);
    } else {
      File extractedFile = File('/tmp/' + archiveFile.name);
      extractedFile.createSync(recursive: true);
      extractedFile.writeAsBytesSync(archiveFile.content);
    }
  }
}
import Foundation
import ZIPFoundation

func extractZipFile(path: String) {
    guard let archive = Archive(url: URL(fileURLWithPath: path), accessMode: .read) else {
        return
    }

    for entry in archive {
        // Insecure: Does not handle symbolic links properly
        let extractedFilePath = "/tmp/\(entry.path)"
        let extractedFileURL = URL(fileURLWithPath: extractedFilePath)

        if entry.type == .symbolicLink {
            do {
                try FileManager.default.createSymbolicLink(at: extractedFileURL, withDestinationURL: entry.destinationURL)
            } catch {
                print("Symbolic link creation failed: \(error.localizedDescription)")
            }
        } else {
            do {
                try FileManager.default.createDirectory(atPath: extractedFileURL.deletingLastPathComponent().path,
                                                        withIntermediateDirectories: true,
                                                        attributes: nil)
                try archive.extract(entry, to: extractedFileURL)
            } catch {
                print("Extraction failed: \(error.localizedDescription)")
            }
        }
    }
}
import java.io.File
import java.nio.file.Files
import java.nio.file.Path
import java.util.zip.ZipInputStream

fun extractZipFile(path: String) {
    val file = File(path)
    val zipInputStream = ZipInputStream(file.inputStream())

    var entry = zipInputStream.nextEntry
    while (entry != null) {
        // Insecure: Does not handle symbolic links properly
        val extractedFile = File("/tmp/" + entry.name)
        if (entry.isSymbolicLink) {
            Files.createSymbolicLink(Path.of(extractedFile.path), Path.of(entry.link))
        } else {
            extractedFile.parentFile.mkdirs()
            extractedFile.outputStream().use { output ->
                zipInputStream.copyTo(output)
            }
        }

        entry = zipInputStream.nextEntry
    }

    zipInputStream.close()
}

ZIP拡張子スプーフィング

ZIP拡張子スプーフィングは、攻撃者がZIPファイル内の悪意のあるファイルの拡張子を偽装し、ユーザーやセキュリティシステムを欺く手法です。ZIPファイルのヘッダーを操作することで、攻撃者はアーカイブ内のファイルの拡張子を変更し、無害に見せかけることができます。しかし、ユーザーがそのファイルを展開したり脆弱なアプリケーションで開いたりすると、悪意のあるペイロードが実行され、不正なコードの実行、マルウェアの感染、その他の悪意のある活動につながる可能性があります。

import 'dart:io';
import 'archive/archive.dart';

void extractZipFile(String path) {
  File file = File(path);
  Archive archive = ZipDecoder().decodeBytes(file.readAsBytesSync());

  for (ArchiveFile archiveFile in archive) {
    // Insecure: the zip decoder parses the filename from the Local File Header which can be manipulated
    // Zipdecoder has to check against the central directory to make sure the extension was not altered
    String extractedFilePath = '/tmp/' + archiveFile.name;
    if (archiveFile.name.endsWith('.zip')) {
      // Extracting a ZIP file within a ZIP file
      extractZipFile(extractedFilePath);
    } else {
      File extractedFile = File(extractedFilePath);
      extractedFile.createSync(recursive: true);
      extractedFile.writeAsBytesSync(archiveFile.content);
    }
  }
}
import java.io.File
import java.util.zip.ZipInputStream

fun extractZipFile(path: String) {
    val file = File(path)
    val zipInputStream = ZipInputStream(file.inputStream())

    var entry = zipInputStream.nextEntry
    while (entry != null) {
        // Insecure: the zip decoder parses the filename from the Local File Header which can be altered
        // Zipdecoder has to check against the central directory to make sure the extension was not altered
        val extractedFile = File("/tmp/" + entry.name)
        if (entry.name.endsWith(".zip")) {
            // Extracting a ZIP file within a ZIP file
            extractZipFile(extractedFile.path)
        } else {
            extractedFile.parentFile.mkdirs()
            extractedFile.outputStream().use { output ->
                zipInputStream.copyTo(output)
            }
        }

        entry = zipInputStream.nextEntry
    }

    zipInputStream.close()
}

推奨事項

ZIPファイルに関連するリスクを軽減するため、以下の事項を検討してください。

  • 抽出する各ZIPエントリについて、標準ライブラリを使用してパスを標準化し、抽出先ディレクトリ内に含まれているかどうかを確認します。
  • ユーザーから提供された入力にディレクトリトラバーサルシーケンスが含まれるのを防ぐため、適切な入力検証とサニタイズを実施します。
import 'dart:io';
import 'archive/archive.dart';

void extractZipFile(String path) {
  File file = File(path);
  Archive archive = ZipDecoder().decodeBytes(file.readAsBytesSync());

  for (ArchiveFile archiveFile in archive) {
    String extractedFilePath = '/tmp/' + sanitizeFilePath(archiveFile.name);

    // Check if the extracted file path is within the allowed directory
    if (isPathWithinAllowedDirectory(extractedFilePath)) {
      File extractedFile = File(extractedFilePath);
      extractedFile.createSync(recursive: true);
      extractedFile.writeAsBytesSync(archiveFile.content);
    }
  }
}

String sanitizeFilePath(String filePath) {
  // Implement logic to sanitize the file path and remove any potentially harmful characters or sequences
  // Return the sanitized file path
}

bool isPathWithinAllowedDirectory(String filePath) {
  // Implement logic to check if the extracted file path is within the allowed directory
  // Return true if the file path is allowed, false otherwise
}
import Foundation

func extractZipFile(path: String) {
    guard let archive = Archive(url: URL(fileURLWithPath: path), accessMode: .read) else {
        return
    }

    let destinationDir = URL(fileURLWithPath: "/tmp/")

    for entry in archive {
        let extractedFilePath = sanitizeFilePath(entry.path)

        // Check if the extracted file path is within the allowed directory
        if isPathWithinAllowedDirectory(extractedFilePath) {
            let extractedFileURL = destinationDir.appendingPathComponent(extractedFilePath)

            do {
                try archive.extract(entry, to: extractedFileURL)
            } catch {
                print("Error extracting file: \(error)")
            }
        }
    }
}

func sanitizeFilePath(_ filePath: String) -> String {
    // Implement logic to sanitize the file path and remove any potentially harmful characters or sequences
    // Return the sanitized file path
}

func isPathWithinAllowedDirectory(_ filePath: String) -> Bool {
    // Implement logic to check if the extracted file path is within the allowed directory
    // Return true if the file path is allowed, false otherwise
}
import java.io.File
import java.util.zip.ZipEntry
import java.util.zip.ZipFile

fun extractZipFile(path: String) {
    val file = File(path)
    val zipFile = ZipFile(file)

    val destinationDir = File("/tmp/")
    val zipEntries = zipFile.entries()

    while (zipEntries.hasMoreElements()) {
        val zipEntry = zipEntries.nextElement()
        val extractedFilePath = sanitizeFilePath(zipEntry.name)

        // Check if the extracted file path is within the allowed directory
        if (isPathWithinAllowedDirectory(extractedFilePath)) {
            val extractedFile = File(destinationDir, extractedFilePath)
            extractedFile.parentFile.mkdirs()
            extractedFile.outputStream().use { outputStream ->
                zipFile.getInputStream(zipEntry).copyTo(outputStream)
            }
        }
    }
    zipFile.close()
}

fun sanitizeFilePath(filePath: String): String {
    // Implement logic to sanitize the file path and remove any potentially harmful characters or sequences
    // Return the sanitized file path
}

fun isPathWithinAllowedDirectory(filePath: String): Boolean {
    // Implement logic to check if the extracted file path is within the allowed directory
    // Return true if the file path is allowed, false otherwise
}

ZIPシンボリックリンク

  • ファイルを展開する前に、ZIPアーカイブ内にシンボリックリンクがないか確認し、展開中に無条件で追跡されないようにします。
  • ディレクトリトラバーサルや機密システムファイルへのアクセスを防ぐため、シンボリックリンクのターゲットを検証し、サニタイズします。
  • シンボリックリンクを安全に処理し、悪意のあるリンクの作成を防ぐ、プラットフォーム固有の関数やライブラリを使用します。
  • 展開プロセスを安全であることが確認されている場所に限定し、その範囲外にシンボリックリンクが作成されることを防ぎます。
  • シンボリックリンクを無視します。
import 'dart:io';
import 'archive/archive.dart';
import 'path';

void extractZipFile(String path) {
  File file = File(path);
  Archive archive = ZipDecoder().decodeBytes(file.readAsBytesSync());

  for (ArchiveFile archiveFile in archive) {
    if (!isSymbolicLink(archiveFile)) {
      // Extract regular file
      String extractedFilePath = '/tmp/' + sanitizePath(archiveFile.name);
      File extractedFile = File(extractedFilePath);
      extractedFile.createSync(recursive: true);
      extractedFile.writeAsBytesSync(archiveFile.content);
    } 
  }
}

bool isSymbolicLink(ArchiveFile archiveFile) {
  // Implement platform-specific logic to check if the file is a symbolic link
  // Return true if it is a symbolic link, false otherwise
} 
import Foundation
import ZIPFoundation

func extractZipFile(path: String) {
    let fileManager = FileManager.default
    guard let archive = Archive(url: URL(fileURLWithPath: path), accessMode: .read) else {
        return
    }

    for entry in archive {
        if !isSymbolicLink(entry) {
            // Extract regular file
            let extractedFilePath = "/tmp/" + sanitizePath(entry.path)
            let extractedFileURL = URL(fileURLWithPath: extractedFilePath)
            fileManager.createFile(atPath: extractedFilePath, contents: entry.data, attributes: nil)
        } 
    }
}

func isSymbolicLink(_ entry: Entry) -> Bool {
    // Implement platform-specific logic to check if the entry is a symbolic link
    // Return true if it is a symbolic link, false otherwise
}
import java.io.File
import java.nio.file.FileSystems
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardCopyOption
import java.nio.file.attribute.PosixFilePermission
import java.util.zip.ZipInputStream

fun extractZipFile(path: String) {
    val file = File(path)
    val zipInput = ZipInputStream(file.inputStream())
    var entry = zipInput.nextEntry
    while (entry != null) {
        if (!isSymbolicLink(entry)) {
            // Extract regular file
            val extractedFilePath = File("/tmp", sanitizePath(entry.name))
            extractedFilePath.parentFile.mkdirs()
            Files.copy(zipInput, extractedFilePath.toPath(), StandardCopyOption.REPLACE_EXISTING)
        } 
        entry = zipInput.nextEntry
    }
}

fun isSymbolicLink(entry: ZipEntry): Boolean {
    // Implement platform-specific logic to check if the entry is a symbolic link
    // Return true if it is a symbolic link, false otherwise
}

ZIP拡張子スプーフィング

  • 抽出されたファイルに対して追加のチェックや検証を実行し、真のファイルタイプが期待される拡張子と一致していることを確認します。
  • ファイルシグネチャまたはマジックナンバーを使用してファイルの内容を検証し、指定された拡張子と比較することを検討してください。
  • 一貫性を確保するため、拡張子とファイルヘッダーの両方に基づくファイルタイプ検証を実施します。
  • ZIPファイルを安全に処理するために特化して設計されたサードパーティ製のライブラリやツールの使用を検討してください。これらは拡張子スプーフィング攻撃に対する組み込みの保護機能を提供している場合があります。
import 'dart:io';
import 'archive/archive.dart';
import 'path';

void extractZipFile(String path) {
  File file = File(path);
  Archive archive = ZipDecoder().decodeBytes(file.readAsBytesSync());

  for (ArchiveFile archiveFile in archive) {
    // Mitigation: Validate the file type by comparing the extension and file header
    if (isFileExtensionValid(archiveFile) && isFileHeaderValid(archiveFile)) {
      String extractedFilePath = '/tmp/' + sanitizePath(archiveFile.name);
      File extractedFile = File(extractedFilePath);
      extractedFile.createSync(recursive: true);
      extractedFile.writeAsBytesSync(archiveFile.content);
    } else {
      // Handle case when file type does not match the expected extension
      print('Invalid file type detected: ${archiveFile.name}');
    }
  }
}

bool isFileExtensionValid(ArchiveFile archiveFile) {
  // Implement logic to validate the file extension against expected extensions
  // Return true if the file extension is valid, false otherwise
}

bool isFileHeaderValid(ArchiveFile archiveFile) {
  // Implement logic to validate the file header and ensure it matches the expected file type
  // Return true if the file header is valid, false otherwise
}
import Foundation
import ZIPFoundation

func extractZipFile(path: String) {
    let fileManager = FileManager.default
    guard let archive = Archive(url: URL(fileURLWithPath: path), accessMode: .read) else {
        return
    }

    for entry in archive {
        // Mitigation: Validate the file type by comparing the extension and file header
        if isFileExtensionValid(entry) && isFileHeaderValid(entry) {
            let extractedFilePath = "/tmp/" + sanitizePath(entry.path)
            let extractedFileURL = URL(fileURLWithPath: extractedFilePath)
            fileManager.createFile(atPath: extractedFilePath, contents: entry.data, attributes: nil)
        } else {
            // Handle case when file type does not match the expected extension
            print("Invalid file type detected: \(entry.path)")
        }
    }
}

func isFileExtensionValid(_ entry: Entry) -> Bool {
    // Implement logic to validate the file extension against expected extensions
    // Return true if the file extension is valid, false otherwise
}

func isFileHeaderValid(_ entry: Entry) -> Bool {
    // Implement logic to validate the file header and ensure it matches the expected file type
    // Return true if the file header is valid, false otherwise
}
import java.io.File
import java.nio.file.FileSystems
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardCopyOption
import java.nio.file.attribute.PosixFilePermission
import java.util.zip.ZipInputStream

fun extractZipFile(path: String) {
    val file = File(path)
    val zipInput = ZipInputStream(file.inputStream())
    var entry = zipInput.nextEntry
    while (entry != null) {
        // Mitigation: Validate the file type by comparing the extension and file header
        if (isFileExtensionValid(entry) && isFileHeaderValid(entry)) {
            val extractedFilePath = File("/tmp", sanitizePath(entry.name))
            extractedFilePath.parentFile.mkdirs()
            Files.copy(zipInput, extractedFilePath.toPath(), StandardCopyOption.REPLACE_EXISTING)
        } else {
            // Handle case when file type does not match the expected extension
            println("Invalid file type detected: ${entry.name}")
        }
        entry = zipInput.nextEntry
    }
}

fun isFileExtensionValid(entry: ZipEntry): Boolean {
    // Implement logic to validate the file extension against expected extensions
    // Return true if the file extension is valid, false otherwise
}

fun isFileHeaderValid(entry: ZipEntry): Boolean {
    // Implement logic to validate the file header and ensure it matches the expected file type
    // Return true if the file header is valid, false otherwise
}

リンク

標準

  • OWASP_MASVS_L1:
    • MSTG_PLATFORM_2
  • OWASP_MASVS_L2:
    • MSTG_PLATFORM_2
  • PCI_STANDARDS:
    • REQ_2_2
    • REQ_6_2
    • REQ_6_3
    • REQ_11_3
  • OWASP_MASVS_v2_1:
    • MASVS_CODE_4
  • SOC2_CONTROLS:
    • CC_2_1
    • CC_4_1
    • CC_7_1
    • CC_7_2
    • CC_7_4
    • CC_7_5
  • HIPAA_CONTROLS:
    • SECURITY212
    • SECURITY213
    • SECURITY255