ZIP Vulnerabilities: Path Traversal, Zip Symbolic Link, and Zip Extension Spoofing
Vulnérabilités ZIP : Traversée de répertoire, Liens symboliques ZIP et Usurpation d'extension ZIP
Description
Les fichiers ZIP, qui sont des archives compressées utilisées pour stocker et transmettre de multiples fichiers, sont un format très adopté pour leur praticité et leur compatibilité sur différentes plateformes. Cependant, comme tout format de fichier numérique, les fichiers ZIP ne sont pas immunisés contre les vulnérabilités. Voici quelques vulnérabilités courantes associées aux fichiers ZIP :
Traversée de répertoire (Path Traversal)
La traversée de répertoire, également connue sous le nom de "directory traversal" ou "directory climbing", est une vulnérabilité permettant à un attaquant d'accéder à des fichiers ou des répertoires en dehors du répertoire d'extraction prévu. Lors de l'extraction d'un fichier ZIP, si le processus d'extraction ne valide pas correctement les chemins d'accès au sein de l'archive, un attaquant peut créer un fichier ZIP malveillant contenant des caractères ou séquences spéciaux qui lui permettent de traverser les répertoires et d'accéder aux fichiers sensibles du système. Cela peut mener à la divulgation non autorisée d'informations sensibles ou même à l'exécution de code à distance.
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()
}
Lien symbolique ZIP
Les liens symboliques, ou symlinks, sont des pointeurs vers des fichiers ou répertoires pouvant être utilisés pour créer des raccourcis ou des références. Toutefois, si le processus d'extraction d'un fichier ZIP ne gère pas correctement les liens symboliques, un attaquant peut forger un fichier ZIP malveillant incluant des liens symboliques qui pointent vers des fichiers ou répertoires sensibles sur le système cible. Lors de l'extraction, ces liens peuvent être suivis, entraînant un accès non autorisé à des fichiers ou répertoires critiques.
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()
}
Usurpation d'extension ZIP
L'usurpation d'extension ZIP est une technique par laquelle un attaquant falsifie l'extension d'un fichier malveillant contenu dans un fichier ZIP pour tromper les utilisateurs et les systèmes de sécurité. En manipulant les en-têtes du fichier ZIP, l'attaquant peut modifier l'extension du fichier au sein de l'archive pour qu'il paraisse inoffensif. Cependant, lorsque l'utilisateur extrait le fichier ou l'ouvre avec une application vulnérable, la charge utile malveillante est exécutée, ce qui peut potentiellement conduire à l'exécution non autorisée de code, une infection par un logiciel malveillant, ou d'autres activités malveillantes.
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()
}
Recommandation
Pour atténuer les risques liés aux fichiers zip, prenez en considération les recommandations suivantes :
- Pour chaque entrée zip à extraire, normalisez le chemin à l'aide d'une bibliothèque standard et vérifiez s'il est contenu dans le répertoire d'extraction.
- Mettez en place une validation et un nettoyage appropriés des entrées pour éviter que les données fournies par l'utilisateur ne contiennent des séquences de traversée de répertoire.
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
}
Lien symbolique ZIP
- Avant d'extraire des fichiers, vérifiez la présence de liens symboliques dans l'archive ZIP et assurez-vous qu'ils ne sont pas suivis aveuglément durant l'extraction.
- Validez et nettoyez la cible du lien symbolique pour prévenir toute traversée de répertoire ou tout accès aux fichiers sensibles du système.
- Utilisez les fonctions ou bibliothèques spécifiques à la plateforme qui gèrent les liens symboliques de manière sécurisée et empêchent la création de liens malveillants.
- Limitez le processus d'extraction à des emplacements reconnus comme sûrs et évitez la création de liens symboliques en dehors de ces limites.
- Ignorez les liens symboliques (symlinks).
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
}
Usurpation d'extension ZIP
- Effectuez des vérifications ou des validations supplémentaires sur les fichiers extraits pour vous assurer que leur véritable type de fichier correspond à l'extension attendue.
- Envisagez d'utiliser les signatures de fichiers ou les "magic numbers" pour vérifier le contenu du fichier et le comparer à l'extension indiquée.
- Mettez en œuvre une vérification du type de fichier basée à la fois sur l'extension et sur l'en-tête du fichier pour garantir la cohérence.
- Envisagez d'utiliser des bibliothèques tierces ou des outils spécifiquement conçus pour gérer les fichiers ZIP de manière sécurisée, car ils peuvent fournir une protection intégrée contre les attaques d'usurpation d'extension.
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
}
Liens
Normes
- 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