Zip Slip Vulnerability in Archive Extraction

Critical Risk File Upload & Path Traversal
zip-sliparchive-extractionpath-traversaljavaarbitrary-file-writecompression

What it is

A critical path traversal vulnerability that occurs during the extraction of archive files (ZIP, TAR, etc.) when filenames within the archive contain path traversal sequences like '../'. This allows attackers to write files outside the intended extraction directory, potentially overwriting critical system files or placing executable files in dangerous locations.

import java.io.*;
import java.util.zip.*;

public class VulnerableZipExtractor {
    
    // VULNERABLE: No path validation during extraction
    public static void extractZip(String zipFilePath, String destDirectory) 
            throws IOException {
        
        File destDir = new File(destDirectory);
        if (!destDir.exists()) {
            destDir.mkdir();
        }
        
        ZipInputStream zipIn = new ZipInputStream(new FileInputStream(zipFilePath));
        ZipEntry entry = zipIn.getNextEntry();
        
        // Extract each entry
        while (entry != null) {
            // DANGEROUS: Direct use of entry name without validation
            String filePath = destDirectory + File.separator + entry.getName();
            
            if (!entry.isDirectory()) {
                // Extract file without any path checks
                extractFile(zipIn, filePath);
            } else {
                // Create directory without validation
                File dir = new File(filePath);
                dir.mkdirs();
            }
            
            zipIn.closeEntry();
            entry = zipIn.getNextEntry();
        }
        
        zipIn.close();
    }
    
    private static void extractFile(ZipInputStream zipIn, String filePath) 
            throws IOException {
        
        // No validation of file path or size
        BufferedOutputStream bos = new BufferedOutputStream(
            new FileOutputStream(filePath));
        
        byte[] bytesIn = new byte[4096];
        int read = 0;
        
        // No limits on extraction size - vulnerable to zip bombs
        while ((read = zipIn.read(bytesIn)) != -1) {
            bos.write(bytesIn, 0, read);
        }
        
        bos.close();
    }
    
    public static void main(String[] args) {
        try {
            extractZip("malicious.zip", "extracted/");
            System.out.println("Extraction completed.");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

/*
Malicious ZIP contents that would exploit this:

1. Entry: "../../../etc/passwd" 
   → Writes to /etc/passwd
   
2. Entry: "..\\..\\..\\windows\\system32\\evil.dll"
   → Writes to Windows system directory
   
3. Entry: "legitimate_folder/../../../usr/bin/malware"
   → Writes executable to system PATH
   
4. Zip bomb: Small compressed file that expands to 1GB+
   → Causes disk space exhaustion
*/
import java.io.*;
import java.nio.file.*;
import java.util.zip.*;
import java.util.concurrent.TimeUnit;

public class SecureZipExtractor {
    
    // Security limits
    private static final long MAX_SIZE = 100 * 1024 * 1024; // 100MB total
    private static final long MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB per file
    private static final int MAX_ENTRIES = 1000;
    private static final int MAX_DEPTH = 10;
    private static final long EXTRACTION_TIMEOUT = 30; // seconds
    
    public static void extractZipSecurely(String zipFilePath, String destDirectory) 
            throws IOException, SecurityException {
        
        Path destPath = Paths.get(destDirectory).toAbsolutePath().normalize();
        
        // Create destination if it doesn't exist
        if (!Files.exists(destPath)) {
            Files.createDirectories(destPath);
        }
        
        // Validate destination is a directory
        if (!Files.isDirectory(destPath)) {
            throw new IllegalArgumentException("Destination must be a directory");
        }
        
        long startTime = System.currentTimeMillis();
        
        try (ZipInputStream zipIn = new ZipInputStream(
                new BufferedInputStream(new FileInputStream(zipFilePath)))) {
            
            ZipEntry entry;
            int entryCount = 0;
            long totalExtractedSize = 0;
            
            while ((entry = zipIn.getNextEntry()) != null) {
                // Check timeout
                if (System.currentTimeMillis() - startTime > 
                    TimeUnit.SECONDS.toMillis(EXTRACTION_TIMEOUT)) {
                    throw new SecurityException("Extraction timeout exceeded");
                }
                
                // Validate entry count
                if (++entryCount > MAX_ENTRIES) {
                    throw new SecurityException("Too many entries in archive: " + entryCount);
                }
                
                // Validate and resolve entry path
                Path entryPath = validateAndResolveEntryPath(entry, destPath);
                
                if (entry.isDirectory()) {
                    createSecureDirectory(entryPath);
                } else {
                    long fileSize = extractFileSecurely(zipIn, entryPath, entry);
                    totalExtractedSize += fileSize;
                    
                    // Check total size limit
                    if (totalExtractedSize > MAX_SIZE) {
                        throw new SecurityException("Total extraction size exceeded: " + 
                            totalExtractedSize);
                    }
                }
                
                zipIn.closeEntry();
            }
        }
        
        System.out.println("Secure extraction completed. Entries: " + entryCount + 
            ", Total size: " + totalExtractedSize + " bytes");
    }
    
    private static Path validateAndResolveEntryPath(ZipEntry entry, Path destDir) 
            throws SecurityException {
        
        String entryName = entry.getName();
        
        // Basic validation
        if (entryName == null || entryName.trim().isEmpty()) {
            throw new SecurityException("Empty entry name");
        }
        
        // Check for dangerous patterns
        if (entryName.contains("../") || entryName.contains("..\\")) {
            throw new SecurityException("Path traversal detected: " + entryName);
        }
        
        // Check for absolute paths
        if (entryName.startsWith("/") || entryName.startsWith("\\") || 
            (entryName.length() > 1 && entryName.charAt(1) == ':')) {
            throw new SecurityException("Absolute path detected: " + entryName);
        }
        
        // Check directory depth
        int depth = entryName.split("[/\\\\]").length;
        if (depth > MAX_DEPTH) {
            throw new SecurityException("Directory nesting too deep: " + depth);
        }
        
        // Sanitize filename
        String sanitized = sanitizeEntryName(entryName);
        
        // Resolve path and validate it's within destination
        Path entryPath = destDir.resolve(sanitized).normalize();
        
        if (!entryPath.startsWith(destDir)) {
            throw new SecurityException(
                "Path traversal attempt - resolved path outside destination: " + 
                entryPath + " not within " + destDir);
        }
        
        return entryPath;
    }
    
    private static String sanitizeEntryName(String entryName) {
        // Remove dangerous characters and normalize separators
        return entryName
            .replaceAll("[<>:\"|?*]", "")  // Windows forbidden chars
            .replaceAll("\\0", "")          // Null bytes
            .replace("\\\\", "/")          // Normalize separators
            .replaceAll("/{2,}", "/")       // Remove multiple slashes
            .trim();
    }
    
    private static void createSecureDirectory(Path dirPath) throws IOException {
        // Create directory with secure permissions
        if (!Files.exists(dirPath)) {
            Files.createDirectories(dirPath);
            
            // Set restrictive permissions on Unix systems
            if (!System.getProperty("os.name").toLowerCase().contains("windows")) {
                try {
                    Files.setPosixFilePermissions(dirPath, Set.of(
                        PosixFilePermission.OWNER_READ,
                        PosixFilePermission.OWNER_WRITE,
                        PosixFilePermission.OWNER_EXECUTE
                    ));
                } catch (UnsupportedOperationException e) {
                    // Filesystem doesn't support POSIX permissions
                }
            }
        }
    }
    
    private static long extractFileSecurely(ZipInputStream zipIn, Path filePath, 
            ZipEntry entry) throws IOException, SecurityException {
        
        // Validate file size from entry (if available)
        long declaredSize = entry.getSize();
        if (declaredSize > MAX_FILE_SIZE) {
            throw new SecurityException("File too large: " + declaredSize + " bytes");
        }
        
        // Ensure parent directory exists
        Path parentDir = filePath.getParent();
        if (parentDir != null && !Files.exists(parentDir)) {
            createSecureDirectory(parentDir);
        }
        
        // Extract with monitoring
        try (OutputStream fileOut = Files.newOutputStream(filePath);
             BufferedOutputStream bufferedOut = new BufferedOutputStream(fileOut)) {
            
            byte[] buffer = new byte[8192];
            long totalBytesRead = 0;
            int bytesRead;
            
            while ((bytesRead = zipIn.read(buffer)) != -1) {
                totalBytesRead += bytesRead;
                
                // Check file size limit
                if (totalBytesRead > MAX_FILE_SIZE) {
                    // Cleanup partial file
                    try {
                        Files.deleteIfExists(filePath);
                    } catch (IOException e) {
                        // Log but don't throw
                    }
                    throw new SecurityException("File size limit exceeded during extraction");
                }
                
                bufferedOut.write(buffer, 0, bytesRead);
            }
            
            bufferedOut.flush();
            
            // Verify final size matches declaration if provided
            if (declaredSize >= 0 && totalBytesRead != declaredSize) {
                System.out.println("Warning: File size mismatch. Declared: " + 
                    declaredSize + ", Actual: " + totalBytesRead);
            }
            
            return totalBytesRead;
        }
    }
    
    // Example usage with error handling
    public static void main(String[] args) {
        if (args.length != 2) {
            System.out.println("Usage: java SecureZipExtractor <zip-file> <destination>");
            return;
        }
        
        try {
            extractZipSecurely(args[0], args[1]);
            System.out.println("Secure extraction completed successfully.");
        } catch (SecurityException e) {
            System.err.println("Security violation: " + e.getMessage());
        } catch (IOException e) {
            System.err.println("IO error: " + e.getMessage());
        }
    }
    
    // Utility method to validate ZIP file before extraction
    public static boolean validateZipFile(String zipFilePath) {
        try (ZipFile zipFile = new ZipFile(zipFilePath)) {
            // Basic validation - can be expanded
            return zipFile.size() <= MAX_ENTRIES;
        } catch (IOException e) {
            return false;
        }
    }
}

💡 Why This Fix Works

The vulnerable code directly uses ZIP entry names to create file paths, allowing path traversal attacks. The secure version implements comprehensive validation, resource limits, and proper path resolution to prevent zip slip vulnerabilities.

import zipfile
import os

# VULNERABLE: No path validation
def extract_zip(zip_path, extract_to):
    with zipfile.ZipFile(zip_path, 'r') as zip_ref:
        for member in zip_ref.infolist():
            # DANGEROUS: Direct extraction without validation
            zip_ref.extract(member, extract_to)
            
            # This allows:
            # member.filename = "../../../etc/passwd"
            # Results in overwriting system files
            
    print(f"Extracted {zip_path} to {extract_to}")

# VULNERABLE: Manual extraction
def manual_extract(zip_path, extract_to):
    with zipfile.ZipFile(zip_path, 'r') as zip_file:
        for filename in zip_file.namelist():
            # No validation of filename
            file_path = os.path.join(extract_to, filename)
            
            with zip_file.open(filename) as source:
                with open(file_path, 'wb') as target:
                    # No size limits - vulnerable to zip bombs
                    target.write(source.read())
import zipfile
import os
import tempfile
from pathlib import Path
import time

class SecureZipExtractor:
    def __init__(self):
        self.max_size = 100 * 1024 * 1024  # 100MB
        self.max_file_size = 50 * 1024 * 1024  # 50MB per file
        self.max_entries = 1000
        self.max_depth = 10
        self.timeout = 30  # seconds
        
    def extract_zip_securely(self, zip_path, extract_to):
        """Securely extract ZIP file with comprehensive validation"""
        
        extract_path = Path(extract_to).resolve()
        extract_path.mkdir(parents=True, exist_ok=True)
        
        if not extract_path.is_dir():
            raise ValueError("Destination must be a directory")
        
        start_time = time.time()
        
        with zipfile.ZipFile(zip_path, 'r') as zip_file:
            entries = zip_file.infolist()
            
            if len(entries) > self.max_entries:
                raise SecurityError(f"Too many entries: {len(entries)}")
            
            total_size = 0
            
            for i, entry in enumerate(entries):
                # Check timeout
                if time.time() - start_time > self.timeout:
                    raise SecurityError("Extraction timeout exceeded")
                
                # Validate entry
                safe_path = self._validate_entry_path(entry.filename, extract_path)
                
                if entry.is_dir():
                    safe_path.mkdir(parents=True, exist_ok=True)
                    self._set_secure_permissions(safe_path)
                else:
                    file_size = self._extract_file_securely(
                        zip_file, entry, safe_path
                    )
                    total_size += file_size
                    
                    if total_size > self.max_size:
                        raise SecurityError(
                            f"Total extraction size exceeded: {total_size}"
                        )
                
                # Progress reporting
                if i % 100 == 0:
                    print(f"Processed {i+1}/{len(entries)} entries")
        
        print(f"Secure extraction completed. Total size: {total_size} bytes")
        return total_size
    
    def _validate_entry_path(self, entry_name, base_path):
        """Validate and resolve entry path safely"""
        
        if not entry_name or not entry_name.strip():
            raise SecurityError("Empty entry name")
        
        # Check for dangerous patterns
        dangerous_patterns = [
            '../', '..\\', '..\\\\',
            '/..', '\\..', '\\\\..'
        ]
        
        for pattern in dangerous_patterns:
            if pattern in entry_name:
                raise SecurityError(f"Path traversal detected: {entry_name}")
        
        # Check for absolute paths
        if entry_name.startswith(('/', '\\')) or \
           (len(entry_name) > 1 and entry_name[1] == ':'):
            raise SecurityError(f"Absolute path detected: {entry_name}")
        
        # Check directory depth
        depth = len([p for p in entry_name.split('/') if p and p != '.'])
        if depth > self.max_depth:
            raise SecurityError(f"Directory nesting too deep: {depth}")
        
        # Sanitize and resolve path
        sanitized = self._sanitize_filename(entry_name)
        safe_path = (base_path / sanitized).resolve()
        
        # Ensure path is within base directory
        try:
            safe_path.relative_to(base_path)
        except ValueError:
            raise SecurityError(
                f"Path traversal attempt: {safe_path} not within {base_path}"
            )
        
        return safe_path
    
    def _sanitize_filename(self, filename):
        """Remove dangerous characters from filename"""
        import re
        
        # Remove null bytes and control characters
        sanitized = ''.join(char for char in filename if ord(char) >= 32)
        
        # Remove dangerous characters
        sanitized = re.sub(r'[<>:"|?*]', '', sanitized)
        
        # Normalize path separators
        sanitized = sanitized.replace('\\', '/')
        
        # Remove multiple consecutive slashes
        sanitized = re.sub(r'/+', '/', sanitized)
        
        return sanitized.strip()
    
    def _extract_file_securely(self, zip_file, entry, output_path):
        """Extract individual file with size validation"""
        
        declared_size = entry.file_size
        if declared_size > self.max_file_size:
            raise SecurityError(f"File too large: {declared_size} bytes")
        
        # Ensure parent directory exists
        output_path.parent.mkdir(parents=True, exist_ok=True)
        
        with zip_file.open(entry) as source:
            with open(output_path, 'wb') as target:
                total_bytes = 0
                chunk_size = 8192
                
                while True:
                    chunk = source.read(chunk_size)
                    if not chunk:
                        break
                    
                    total_bytes += len(chunk)
                    
                    # Check size limit
                    if total_bytes > self.max_file_size:
                        # Clean up partial file
                        try:
                            output_path.unlink()
                        except OSError:
                            pass
                        raise SecurityError(
                            "File size limit exceeded during extraction"
                        )
                    
                    target.write(chunk)
        
        # Set secure permissions
        self._set_secure_permissions(output_path)
        
        # Verify final size
        if declared_size > 0 and total_bytes != declared_size:
            print(f"Warning: Size mismatch for {output_path.name}. "
                  f"Declared: {declared_size}, Actual: {total_bytes}")
        
        return total_bytes
    
    def _set_secure_permissions(self, path):
        """Set secure file permissions"""
        try:
            if path.is_file():
                # Read/write for owner only
                path.chmod(0o600)
            elif path.is_dir():
                # Read/write/execute for owner only
                path.chmod(0o700)
        except OSError:
            # Permissions not supported on this filesystem
            pass
    
    def validate_zip_before_extraction(self, zip_path):
        """Pre-validate ZIP file before extraction"""
        try:
            with zipfile.ZipFile(zip_path, 'r') as zip_file:
                # Check if it's a valid ZIP
                if zip_file.testzip() is not None:
                    raise SecurityError("Corrupted ZIP file")
                
                # Check number of entries
                entries = zip_file.infolist()
                if len(entries) > self.max_entries:
                    raise SecurityError(f"Too many entries: {len(entries)}")
                
                # Check total declared size
                total_declared = sum(entry.file_size for entry in entries)
                if total_declared > self.max_size:
                    raise SecurityError(f"Total size too large: {total_declared}")
                
                # Check for suspicious patterns
                for entry in entries:
                    if '../' in entry.filename or '..\\'  in entry.filename:
                        raise SecurityError(
                            f"Suspicious entry detected: {entry.filename}"
                        )
                
                return True
        except zipfile.BadZipFile:
            raise SecurityError("Invalid ZIP file")

class SecurityError(Exception):
    """Custom exception for security violations"""
    pass

# Example usage
def main():
    extractor = SecureZipExtractor()
    
    try:
        # Validate before extraction
        extractor.validate_zip_before_extraction('archive.zip')
        
        # Perform secure extraction
        total_size = extractor.extract_zip_securely(
            'archive.zip', 
            'safe_extraction_dir'
        )
        print(f"Successfully extracted {total_size} bytes")
        
    except SecurityError as e:
        print(f"Security violation: {e}")
    except Exception as e:
        print(f"Extraction failed: {e}")

if __name__ == '__main__':
    main()

💡 Why This Fix Works

The vulnerable Python code directly extracts ZIP entries without validation. The secure version implements comprehensive path validation, resource limits, and proper error handling to prevent zip slip attacks.

Why it happens

The most critical flaw is extracting archive entries without validating their paths. Archives can contain entries with malicious names like '../../../etc/passwd' or '..\\..\\..\\windows\\system32\\evil.dll' that, when extracted, write files outside the intended directory. This is especially dangerous because the extraction process has the application's file system privileges.

Root causes

Unvalidated Archive Entry Names

The most critical flaw is extracting archive entries without validating their paths. Archives can contain entries with malicious names like '../../../etc/passwd' or '..\\..\\..\\windows\\system32\\evil.dll' that, when extracted, write files outside the intended directory. This is especially dangerous because the extraction process has the application's file system privileges.

Preview example – JAVA
// VULNERABLE: Direct extraction without path validation
import java.util.zip.*;
import java.io.*;

public void extractZip(String zipFile, String destDir) throws IOException {
    ZipInputStream zis = new ZipInputStream(new FileInputStream(zipFile));
    ZipEntry entry;
    
    while ((entry = zis.getNextEntry()) != null) {
        // DANGEROUS: Using entry name directly
        File outFile = new File(destDir + File.separator + entry.getName());
        
        // Attacker can use: "../../../etc/crontab"
        // Results in writing to: /etc/crontab
        FileOutputStream fos = new FileOutputStream(outFile);
        // ... write data
    }
}

Insufficient Canonical Path Validation

Applications that perform basic path validation but fail to use canonical path resolution can be bypassed. Attackers can use various encoding techniques, symlinks, or platform-specific path separators to circumvent validation. Without proper canonicalization, paths like 'folder/../../../etc/passwd' may pass basic checks but still result in path traversal.

Preview example – JAVA
// VULNERABLE: Weak path validation
public boolean isValidPath(String entryName, String destDir) {
    // Basic check that can be bypassed
    if (entryName.contains("../")) {
        return false;
    }
    
    // Bypasses:
    // "..\\" (Windows separators)
    // "foo/../../../etc/passwd" (after folder)
    // "%2e%2e%2f" (URL encoded)
    // Symlink attacks
    
    return true;
}

Platform-Specific Path Handling Issues

Different operating systems handle file paths differently, creating bypass opportunities. Windows systems use backslashes and support alternate data streams, while Unix systems use forward slashes and support symbolic links. Applications that only validate one platform's conventions can be exploited when deployed on different systems.

Preview example – JAVA
// VULNERABLE: Platform-specific bypass
public void extractEntry(ZipEntry entry, String baseDir) {
    String name = entry.getName();
    
    // Only checks for Unix-style traversal
    if (name.contains("../")) {
        throw new SecurityException("Path traversal detected");
    }
    
    // Windows bypasses:
    // "..\\..\\..\\windows\\system32\\malware.exe"
    // "file.txt:ads" (Alternate Data Stream)
    // "C:\\..\\..\\etc\\passwd" (Absolute paths)
    
    File outputFile = new File(baseDir, name);
    // Extraction continues...
}

Archive Bomb and Resource Exhaustion

Beyond path traversal, malicious archives can contain zip bombs or consume excessive resources during extraction. Large files, deeply nested directories, or archives with extremely high compression ratios can cause denial of service, disk space exhaustion, or memory exhaustion during the extraction process.

Preview example – JAVA
// VULNERABLE: No resource limits during extraction
public void extractArchive(ZipInputStream zis) throws IOException {
    ZipEntry entry;
    
    while ((entry = zis.getNextEntry()) != null) {
        // No size limits - vulnerable to zip bombs
        // No extraction time limits
        // No nested directory depth limits
        
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        byte[] buffer = new byte[4096];
        int len;
        
        // Attacker can create 10GB file that extracts to 1TB
        while ((len = zis.read(buffer)) > 0) {
            baos.write(buffer, 0, len); // Unlimited memory allocation
        }
    }
}

Fixes

1

Implement Comprehensive Path Validation

Always validate and canonicalize archive entry paths before extraction. Resolve canonical paths, check for path traversal sequences, validate against the intended extraction directory, and reject any entries that would write outside the target directory. Use platform-independent validation logic.

View implementation – JAVA
import java.nio.file.*;
import java.util.zip.*;

public class SecureZipExtractor {
    private static final int MAX_ENTRIES = 1000;
    private static final long MAX_SIZE = 100 * 1024 * 1024; // 100MB
    private static final int MAX_DEPTH = 10;
    
    public void extractZip(String zipFile, String destDir) throws IOException {
        Path destPath = Paths.get(destDir).toAbsolutePath().normalize();
        
        try (ZipInputStream zis = new ZipInputStream(new FileInputStream(zipFile))) {
            ZipEntry entry;
            int entryCount = 0;
            long totalSize = 0;
            
            while ((entry = zis.getNextEntry()) != null) {
                // Validate entry
                validateZipEntry(entry, destPath, ++entryCount, totalSize);
                
                Path entryPath = validateAndResolvePath(entry.getName(), destPath);
                
                if (entry.isDirectory()) {
                    Files.createDirectories(entryPath);
                } else {
                    // Ensure parent directory exists
                    Files.createDirectories(entryPath.getParent());
                    
                    // Extract file with size limit
                    totalSize += extractFile(zis, entryPath, entry.getSize());
                }
            }
        }
    }
    
    private Path validateAndResolvePath(String entryName, Path destDir) {
        // Remove any dangerous characters
        String sanitized = entryName.replaceAll("[^a-zA-Z0-9/._-]", "");
        
        // Convert to Path and resolve
        Path entryPath = destDir.resolve(sanitized).normalize();
        
        // Ensure the resolved path is within destination directory
        if (!entryPath.startsWith(destDir)) {
            throw new SecurityException("Path traversal attempt: " + entryName);
        }
        
        return entryPath;
    }
}
2

Implement Resource Limits and Monitoring

Protect against zip bombs and resource exhaustion by implementing strict limits on extraction. Set maximum file sizes, total extraction size, number of entries, directory depth, and extraction time. Monitor resource usage during extraction and abort if limits are exceeded.

View implementation – JAVA
private long extractFile(InputStream inputStream, Path outputPath, long entrySize) 
        throws IOException {
    final long MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB per file
    final int BUFFER_SIZE = 8192;
    
    // Validate declared size
    if (entrySize > MAX_FILE_SIZE) {
        throw new SecurityException("File too large: " + entrySize);
    }
    
    try (OutputStream fos = Files.newOutputStream(outputPath)) {
        byte[] buffer = new byte[BUFFER_SIZE];
        long totalBytesRead = 0;
        int bytesRead;
        
        while ((bytesRead = inputStream.read(buffer)) != -1) {
            totalBytesRead += bytesRead;
            
            // Check against both declared and actual size
            if (totalBytesRead > MAX_FILE_SIZE) {
                throw new SecurityException("File extraction size exceeded limit");
            }
            
            fos.write(buffer, 0, bytesRead);
        }
        
        // Verify final size matches declaration (if provided)
        if (entrySize > 0 && totalBytesRead != entrySize) {
            throw new SecurityException("File size mismatch");
        }
        
        return totalBytesRead;
    }
}

private void validateZipEntry(ZipEntry entry, Path destDir, int entryCount, long totalSize) {
    // Limit number of entries
    if (entryCount > MAX_ENTRIES) {
        throw new SecurityException("Too many archive entries");
    }
    
    // Limit total extraction size
    if (totalSize > MAX_SIZE) {
        throw new SecurityException("Archive too large");
    }
    
    // Limit directory depth
    String name = entry.getName();
    int depth = name.split("/").length;
    if (depth > MAX_DEPTH) {
        throw new SecurityException("Directory nesting too deep");
    }
    
    // Check for suspicious patterns
    if (name.contains("../") || name.contains("..\\") || 
        name.startsWith("/") || name.contains(":")) {
        throw new SecurityException("Suspicious entry name: " + name);
    }
}
3

Use Secure Archive Libraries

Leverage security-enhanced archive libraries that provide built-in protection against zip slip and other archive-based attacks. Libraries like Apache Commons Compress with security features, or dedicated secure extraction libraries, can help prevent common pitfalls.

View implementation – JAVA
import org.apache.commons.compress.archivers.zip.*;
import org.apache.commons.compress.utils.IOUtils;

public class SecureApacheExtractor {
    public void extractZipSecurely(File zipFile, File destDir) throws IOException {
        try (ZipFile zip = new ZipFile(zipFile)) {
            Enumeration<ZipArchiveEntry> entries = zip.getEntries();
            
            while (entries.hasMoreElements()) {
                ZipArchiveEntry entry = entries.nextElement();
                
                // Validate entry name
                if (!isValidEntryName(entry.getName())) {
                    throw new SecurityException("Invalid entry: " + entry.getName());
                }
                
                File destFile = new File(destDir, entry.getName());
                
                // Critical: Validate canonical path
                String destDirCanonical = destDir.getCanonicalPath();
                String destFileCanonical = destFile.getCanonicalPath();
                
                if (!destFileCanonical.startsWith(destDirCanonical + File.separator)) {
                    throw new SecurityException("Path traversal: " + entry.getName());
                }
                
                if (entry.isDirectory()) {
                    destFile.mkdirs();
                } else {
                    // Ensure parent exists
                    destFile.getParentFile().mkdirs();
                    
                    try (InputStream is = zip.getInputStream(entry);
                         FileOutputStream fos = new FileOutputStream(destFile)) {
                        
                        // Use IOUtils.copy with size limits
                        long copied = IOUtils.copy(is, fos, 50 * 1024 * 1024); // 50MB limit
                        
                        if (copied > 50 * 1024 * 1024) {
                            throw new SecurityException("File too large");
                        }
                    }
                }
            }
        }
    }
    
    private boolean isValidEntryName(String name) {
        // Comprehensive validation
        if (name == null || name.trim().isEmpty()) return false;
        if (name.contains("../") || name.contains("..\\")) return false;
        if (name.startsWith("/") || name.startsWith("\\")) return false;
        if (name.contains(":") && System.getProperty("os.name").toLowerCase().contains("windows")) {
            return false; // Prevent absolute paths on Windows
        }
        return true;
    }
}
4

Implement Sandboxed Extraction Environment

Extract archives in isolated environments such as containers, chroot jails, or temporary directories with restricted permissions. This limits the impact of successful zip slip attacks and provides additional layers of protection against malicious archive content.

View implementation – JAVA
import java.nio.file.attribute.PosixFilePermission;
import java.util.Set;

public class SandboxedExtractor {
    private Path createSecureExtractionDir() throws IOException {
        // Create temporary directory with restricted permissions
        Path tempDir = Files.createTempDirectory("secure_extract_");
        
        // Set restrictive permissions (Unix)
        if (!System.getProperty("os.name").toLowerCase().contains("windows")) {
            Set<PosixFilePermission> permissions = Set.of(
                PosixFilePermission.OWNER_READ,
                PosixFilePermission.OWNER_WRITE,
                PosixFilePermission.OWNER_EXECUTE
            );
            Files.setPosixFilePermissions(tempDir, permissions);
        }
        
        // Register for cleanup on exit
        tempDir.toFile().deleteOnExit();
        
        return tempDir;
    }
    
    public Path extractToSandbox(String archivePath) throws IOException {
        Path sandboxDir = createSecureExtractionDir();
        
        try {
            // Perform extraction in sandbox
            extractZip(archivePath, sandboxDir.toString());
            
            // Post-extraction validation
            validateExtractedContent(sandboxDir);
            
            return sandboxDir;
            
        } catch (Exception e) {
            // Cleanup on failure
            cleanupDirectory(sandboxDir);
            throw e;
        }
    }
    
    private void validateExtractedContent(Path extractDir) throws IOException {
        Files.walk(extractDir)
            .forEach(path -> {
                try {
                    // Validate each extracted file
                    if (Files.isRegularFile(path)) {
                        // Check file size
                        if (Files.size(path) > 100 * 1024 * 1024) {
                            throw new SecurityException("Extracted file too large: " + path);
                        }
                        
                        // Check for executable permissions
                        if (Files.isExecutable(path)) {
                            throw new SecurityException("Executable file detected: " + path);
                        }
                    }
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            });
    }
    
    private void cleanupDirectory(Path dir) {
        try {
            Files.walk(dir)
                .sorted(Comparator.reverseOrder())
                .map(Path::toFile)
                .forEach(File::delete);
        } catch (IOException e) {
            // Log cleanup failure
            System.err.println("Failed to cleanup directory: " + e.getMessage());
        }
    }
}

Detect This Vulnerability in Your Code

Sourcery automatically identifies zip slip vulnerability in archive extraction and many other security issues in your codebase.