From 31fa36c82aa661fc6a49ebfc389766f3443b774c Mon Sep 17 00:00:00 2001 From: Thomas Schmidt Date: Mon, 1 Jun 2026 16:12:36 +0200 Subject: [PATCH] Fix multiple bugs and improve robustness - Fix date format in report(): H:m:s -> H:i:s (m=month, i=minutes) - Fix loadWhitelists(): skip lines shorter than 32 chars to avoid empty hash entries - Fix updateCombinedWhitelist(): return false on hash mismatch after download - Fix scan(): handle file_get_contents() failure for unreadable files - Fix scanFunc_RE(): suppress and handle invalid regex patterns gracefully - Fix addWordpressChecksums(): validate version format, handle network/JSON errors, support both API response formats - Add 30s HTTP timeout to updateCombinedWhitelist() network requests - Fix composer.json minimum PHP version: 5.2 -> 5.3 (anonymous functions require 5.3) - Add .gitignore for .idea/, whitelist.dat, vendor/ Co-Authored-By: Claude Sonnet 4.6 --- .gitignore | 3 +++ composer.json | 2 +- scan.php | 53 ++++++++++++++++++++++++++++++++++++++++----------- 3 files changed, 46 insertions(+), 12 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4877650 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.idea/ +whitelist.dat +vendor/ diff --git a/composer.json b/composer.json index 51a6491..6e62515 100644 --- a/composer.json +++ b/composer.json @@ -6,7 +6,7 @@ "license": "GPL-3.0", "homepage": "https://github.com/scr34m/php-malware-scanner", "require": { - "php": ">=5.2.0" + "php": ">=5.3.0" }, "autoload": { }, diff --git a/scan.php b/scan.php index af1207e..a1e4074 100644 --- a/scan.php +++ b/scan.php @@ -203,7 +203,10 @@ class MalwareScanner $fp = fopen($file, 'r'); while (!feof($fp)) { $line = fgets($fp); - $this->whitelist[] = substr($line, 0, 32); + $hash = substr($line, 0, 32); + if (strlen($hash) === 32) { + $this->whitelist[] = $hash; + } } fclose($fp); } @@ -212,16 +215,32 @@ class MalwareScanner public function addWordpressChecksums($wp_version) { - $apiurl = 'https://api.wordpress.org/core/checksums/1.0/?version=' . $wp_version; - $json = json_decode(file_get_contents($apiurl)); - $checksums = $json->checksums; + if (!preg_match('/^\d+\.\d+(\.\d+)?$/', $wp_version)) { + $this->error('Invalid WordPress version format: ' . $wp_version); + exit(-1); + } - if ($checksums->$wp_version == false) { #no checksum returned + $apiurl = 'https://api.wordpress.org/core/checksums/1.0/?version=' . $wp_version; + $raw = file_get_contents($apiurl); + if ($raw === false) { $this->error('Cannot load wordpress checksums from: ' . $apiurl); exit(-1); } - foreach ($checksums->$wp_version as $file => $checksum) { + $json = json_decode($raw); + if ($json === null || !isset($json->checksums)) { + $this->error('Invalid response from WordPress checksums API'); + exit(-1); + } + + $checksums = $json->checksums; + if ($checksums === false || empty((array)$checksums)) { + $this->error('No checksums returned for WordPress version: ' . $wp_version); + exit(-1); + } + + $entries = isset($checksums->$wp_version) ? $checksums->$wp_version : $checksums; + foreach ($entries as $file => $checksum) { $this->whitelist[] = $checksum; } } @@ -623,8 +642,8 @@ class MalwareScanner private function report($start, $dir) { $end = time(); - echo 'Start time: ' . date('Y-m-d H:m:s', $start) . PHP_EOL; - echo 'End time: ' . date('Y-m-d H:m:s', $end) . PHP_EOL; + echo 'Start time: ' . date('Y-m-d H:i:s', $start) . PHP_EOL; + echo 'End time: ' . date('Y-m-d H:i:s', $end) . PHP_EOL; echo 'Total execution time: ' . ($end - $start) . PHP_EOL; echo 'Base directory: ' . $dir . PHP_EOL; echo 'Total directories scanned: ' . $this->stat['directories'] . PHP_EOL; @@ -680,6 +699,12 @@ class MalwareScanner { $this->stat['files_scanned']++; $fileContent = file_get_contents($path); + if ($fileContent === false) { + if (!$this->flagHideErr) { + echo $this->ANSI_RED . '# ER' . $this->ANSI_OFF . ' # {' . $path . '} (unreadable)' . PHP_EOL; + } + return false; + } $found = false; $inWhitelist = false; $hash = md5($fileContent); @@ -725,7 +750,10 @@ class MalwareScanner //Patterns will match multiple lines, though you can use ^$ to match the beginning and end of a line. private function scanFunc_RE(&$pattern, &$content) { - $ret = preg_match('/' . $pattern . '/im', $content, $match, PREG_OFFSET_CAPTURE); + $ret = @preg_match('/' . $pattern . '/im', $content, $match, PREG_OFFSET_CAPTURE); + if ($ret === false) { + return false; + } if ($ret) { return $match[0][1]; } @@ -790,7 +818,9 @@ class MalwareScanner private function updateCombinedWhitelist($url = 'https://scr34m.github.io/php-malware-scanner') { - $latest_hash = trim(file_get_contents($url . '/database/compressed.sha256')); + $ctx = stream_context_create(array('http' => array('timeout' => 30))); + + $latest_hash = trim(file_get_contents($url . '/database/compressed.sha256', false, $ctx)); if ($latest_hash === false) { $this->error('Unable to download database checksum'); return false; @@ -809,7 +839,7 @@ class MalwareScanner } if ($download) { - $data = file_get_contents($url . '/database/compressed.dat'); + $data = file_get_contents($url . '/database/compressed.dat', false, $ctx); if ($data === false) { $this->error('Unable to download database'); return false; @@ -819,6 +849,7 @@ class MalwareScanner $hash = hash_file('sha256', $file); if ($hash != $latest_hash) { $this->error('Downloaded database hash mismatch'); + return false; } }