ZIP Vulnerabilities: Path Traversal, Zip Symbolic Link, and Zip Extension Spoofing
Vulnerabilidades ZIP: Path Traversal, Enlaces simbólicos y Falsificación de extensión
Descripción
Los archivos ZIP, que son archivos comprimidos utilizados para almacenar y transmitir múltiples archivos, son un formato de archivo ampliamente adoptado debido a su conveniencia y compatibilidad en diferentes plataformas. Sin embargo, como cualquier formato de archivo digital, los archivos ZIP no son inmunes a las vulnerabilidades. A continuación se presentan algunas vulnerabilidades comunes asociadas a los archivos ZIP:
Path Traversal (Salto de directorio)
El salto de directorio, también conocido como "directory traversal" o escalada de directorios, es una vulnerabilidad que permite a un atacante acceder a archivos o directorios fuera del directorio de extracción previsto. Al extraer un archivo ZIP, si el proceso de extracción no valida adecuadamente las rutas de los archivos dentro del archivo, un atacante puede crear un archivo ZIP malicioso que contenga secuencias o caracteres especiales que le permitan atravesar directorios y acceder a archivos confidenciales del sistema. Esto puede conducir a la divulgación no autorizada de información confidencial o incluso a la ejecución remota de código.
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()
}
Enlace simbólico ZIP
Los enlaces simbólicos, o symlinks, son punteros a archivos o directorios que pueden usarse para crear accesos directos o referencias. Sin embargo, si un proceso de extracción de archivos ZIP no maneja correctamente los enlaces simbólicos, un atacante puede manipular un archivo ZIP malicioso que incluya enlaces simbólicos apuntando a archivos o directorios confidenciales en el sistema de destino. Tras la extracción, estos enlaces simbólicos pueden seguirse, lo que da lugar a un acceso no autorizado a archivos o directorios críticos.
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()
}
Falsificación de extensión ZIP
La falsificación de extensión ZIP es una técnica en la que un atacante falsifica la extensión de un archivo malicioso dentro de un archivo ZIP para engañar a los usuarios y a los sistemas de seguridad. Al manipular las cabeceras del archivo ZIP, el atacante puede cambiar la extensión del archivo dentro del archivo para que parezca inofensivo. Sin embargo, cuando el usuario extrae el archivo o lo abre con una aplicación vulnerable, se ejecuta la carga maliciosa, lo que puede provocar la ejecución de código no autorizado, la infección de malware u otras actividades maliciosas.
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()
}
Recomendación
Para mitigar los riesgos asociados con los archivos zip, tenga en cuenta lo siguiente:
- Por cada entrada zip a extraer, estandarice la ruta utilizando una biblioteca estándar y compruebe si está contenida en el directorio de extracción.
- Implemente una validación y saneamiento de entrada adecuados para evitar que la entrada proporcionada por el usuario contenga secuencias de salto de directorio.
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
}
Enlace simbólico ZIP
- Antes de extraer archivos, verifique la existencia de enlaces simbólicos dentro del archivo ZIP y asegúrese de que no se sigan ciegamente durante la extracción.
- Valide y sanee el destino del enlace simbólico para evitar el salto de directorios o el acceso a archivos confidenciales del sistema.
- Utilice bibliotecas o funciones específicas de la plataforma que manejen los enlaces simbólicos de forma segura y eviten la creación de enlaces maliciosos.
- Limite el proceso de extracción a ubicaciones seguras conocidas y evite que se creen enlaces simbólicos fuera de esos límites.
- Ignore los enlaces simbólicos (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
}
Falsificación de extensión ZIP
- Realice comprobaciones o validaciones adicionales en los archivos extraídos para garantizar que su verdadero tipo de archivo coincida con la extensión esperada.
- Considere utilizar firmas de archivos o números mágicos ("magic numbers") para verificar el contenido del archivo y compararlo con la extensión indicada.
- Implemente la verificación del tipo de archivo en función tanto de la extensión como de la cabecera del archivo para asegurar la consistencia.
- Considere la posibilidad de utilizar herramientas o bibliotecas de terceros diseñadas específicamente para procesar archivos ZIP de forma segura, ya que pueden ofrecer protección integrada contra los ataques de falsificación de extensiones.
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
}
Enlaces
Estándares
- 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