Files
ntspi-app/app/Containers/VikonIntegration/Tasks/ExtractZipArchiveTask.php
T
2026-04-08 14:14:38 +05:00

120 lines
3.6 KiB
PHP

<?php
namespace App\Containers\VikonIntegration\Tasks;
use ZipArchive;
/**
* Task: Safely extracts ZIP archives with Zip Slip protection
*
* Unlike old vikon_core which used unpackZip() without path validation,
* this task validates every entry to prevent directory traversal attacks.
*/
class ExtractZipArchiveTask
{
/**
* Extract ZIP archive to destination with security checks
*
* @param string $zipPath Absolute path to ZIP file
* @param string $destination Absolute path to extraction directory
* @return bool
* @throws \RuntimeException
*/
public function run(string $zipPath, string $destination): bool
{
if (!file_exists($zipPath)) {
throw new \RuntimeException("ZIP file not found: {$zipPath}");
}
if (!is_writable(dirname($destination))) {
throw new \RuntimeException("Destination directory is not writable: " . dirname($destination));
}
$zip = new ZipArchive;
$openResult = $zip->open($zipPath);
if ($openResult !== true) {
throw new \RuntimeException("Failed to open ZIP archive (code: {$openResult})");
}
// Zip Slip protection: validate every entry
$this->validateZipEntries($zip, $destination);
// Extract with overwrite
$extractResult = $zip->extractTo($destination);
$zip->close();
if (!$extractResult) {
throw new \RuntimeException('Failed to extract ZIP archive');
}
return true;
}
/**
* Validate all ZIP entries to prevent Zip Slip attack
*
* @throws \RuntimeException
*/
private function validateZipEntries(ZipArchive $zip, string $destination): void
{
$realDestination = realpath($destination);
if ($realDestination === false) {
throw new \RuntimeException('Destination directory does not exist');
}
for ($i = 0; $i < $zip->numFiles; $i++) {
$filename = $zip->getNameIndex($i);
// Check for path traversal patterns
if ($this->containsPathTraversal($filename)) {
throw new \RuntimeException(
"Potentially malicious ZIP entry detected: {$filename}"
);
}
// Resolve full path and verify it's within destination
$fullPath = realpath($realDestination . '/' . $filename);
if ($fullPath === false) {
// File doesn't exist yet (will be created), check parent directory
$parentDir = dirname($realDestination . '/' . $filename);
if (strpos($parentDir, $realDestination) !== 0) {
throw new \RuntimeException(
"ZIP entry escapes destination directory: {$filename}"
);
}
} elseif (strpos($fullPath, $realDestination) !== 0) {
throw new \RuntimeException(
"ZIP entry escapes destination directory: {$filename}"
);
}
}
}
/**
* Check if filename contains path traversal patterns
*/
private function containsPathTraversal(string $filename): bool
{
// Check for directory traversal sequences
if (strpos($filename, '..') !== false) {
return true;
}
// Check for absolute paths on Windows
if (preg_match('/^[a-zA-Z]:/', $filename)) {
return true;
}
// Check for absolute paths on Unix
if (strpos($filename, '/') === 0) {
return true;
}
return false;
}
}