open($zipFile); if ($res !== true) { return "{$zipFile} is corrupt or cannot be opened (error code: {$res}).\n"; } $remove_path_escaped = addcslashes($remove_path, "/"); $i = 0; $i2 = 0; $extracted_files = array(); $ignored_files = array(); // Resolve the real extraction root once for Zip Slip checks. $real_extract_root = realpath($extract_path); if ($real_extract_root === false) { $zip->close(); return "Cannot resolve extraction path {$extract_path}.\n"; } $norm_root = str_replace('\\', '/', $real_extract_root); for ($idx = 0; $idx < $zip->numFiles; $idx++) { $filename = $zip->getNameIndex($idx); $file_path = preg_replace("/{$remove_path_escaped}/", '', $filename); $dir_path = preg_replace("/{$remove_path_escaped}/", '', dirname($filename)); // Zip Slip protection: reject entries with path traversal or absolute paths. $norm_filename = str_replace('\\', '/', $filename); if (strpos($norm_filename, '../') !== false || substr($norm_filename, 0, 1) === '/') { continue; } if (isset($blacklist) && is_array($blacklist) && in_array($file_path, $blacklist)) { if (isset($whitelist) && is_array($whitelist) && in_array($filename, $whitelist)) { $ignored_files[$i2] = $file_path; $i2++; } continue; } if (isset($whitelist) && is_array($whitelist) && !in_array($filename, $whitelist)) continue; $completePath = $extract_path . $dir_path; $completeName = $extract_path . $file_path; $escaped_temp_path = str_replace('\\', '\\\\', $temp_path); // For Windows paths backslashes $root = preg_match("#^{$escaped_temp_path}#", $completePath) ? $temp_path : $base_path; $escaped_root = str_replace('\\', '\\\\', $root); $relative_path = preg_replace("#^{$escaped_root}(.*)$#", '$1', $completePath); // Cache this repeated check to avoid duplication. $dirname_in_path = preg_match('/^' . $remove_path_escaped . '/', dirname($filename)); // Walk through path to create non-existing directories. // This won't apply to empty directories – they are created further below. if (!file_exists($completePath) && $dirname_in_path) { $tmp = $root; foreach (preg_split('/(\/|\\\\)/', $relative_path) as $k) { if ($k !== '') { $tmp .= $k . DIRECTORY_SEPARATOR; if (!file_exists($tmp)) { if (!mkdir($tmp, 0777)) { $zip->close(); return "Unable to write folder {$tmp}.\n"; } } } } } if ($dirname_in_path) { if (!preg_match('/\/$/', $completeName)) { // Secondary Zip Slip check: verify the destination path (after normalizing // the string, since the file may not exist yet) stays within the extraction // root. Because we already blocked '../' in the entry name above, this is // belt-and-suspenders for path-manipulation edge-cases. $norm_complete = str_replace('\\', '/', $completeName); if (strpos($norm_complete . '/', $norm_root . '/') !== 0) { continue; // Path escapes the extraction root – skip. } // Stream the entry to disk without loading it entirely into memory. $stream = $zip->getStream($filename); if ($stream === false) { $zip->close(); return "Unable to read entry {$filename} from ZIP.\n"; } $fd = fopen($completeName, 'w+'); if ($fd === false) { fclose($stream); $zip->close(); return "Unable to write file {$completeName}.\n"; } stream_copy_to_stream($stream, $fd); fclose($stream); fclose($fd); $extracted_files[$i]['filename'] = $filename; $i++; } } } $zip->close(); return array('ignored_files' => $ignored_files, 'extracted_files' => $extracted_files); } ?>