: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
require_once(dirname(__FILE__) . '/wordfenceClass.php');
const KNOWN_FILE_CORE = 'core';
const KNOWN_FILE_PLUGIN = 'plugins';
const KNOWN_FILE_THEME = 'themes';
const KNOWN_FILE_OTHER = 'other';
const MAX_QUEUED_RECORDS = 500;
private static $KNOWN_FILE_TYPES = [
private $startTime = false;
private $currentFile = null;
private $knownFileExclude;
private $fileRecords = [];
private $fileRecordCount = 0;
public $totalData = 0; //To do a sanity check, don't use 'du' because it gets sparse files wrong and reports blocks used on disk. Use : find . -type f -ls | awk '{total += $7} END {print total}'
public $stoppedOnFile = false;
private $coreEnabled = false;
private $pluginsEnabled = false;
private $themesEnabled = false;
private $malwareEnabled = false;
private $coreUnknownEnabled = false;
private $knownFiles = false;
private $malwareData = "";
private $coreHashesData = '';
private $haveIssues = array();
private $status = array();
private $possibleMalware = array();
private $alertedOnUnknownWordPressVersion = false;
private $foldersEntered = array();
private $foldersProcessed = array();
private $suspectedFiles = array();
private $indexed = false;
private $currentIndex = 0;
private $coalescingIssues = array();
private $pathMap = array();
* @param wfScanEngine $engine
public function __construct($scannedFiles, $engine, $malwarePrefixesHash, $coreHashesHash, $scanMode) {
$this->scannedFiles = $scannedFiles;
$this->startTime = microtime(true);
$options = $this->engine->scanController()->scanOptions();
if ($options['scansEnabled_core']) { $this->coreEnabled = true; }
if ($options['scansEnabled_plugins']) { $this->pluginsEnabled = true; }
if ($options['scansEnabled_themes']) { $this->themesEnabled = true; }
if ($options['scansEnabled_malware']) { $this->malwareEnabled = true; }
if ($options['scansEnabled_coreUnknown']) { $this->coreUnknownEnabled = true; }
//Doing a delete for now. Later we can optimize this to only scan modified files.
//$this->db->queryWrite("update " . wfDB::networkTable('wfFileMods') . " set oldMD5 = newMD5");
$this->db->truncate(wfDB::networkTable('wfFileMods'));
$this->db->truncate(wfDB::networkTable('wfKnownFileList'));
$this->db->truncate(wfDB::networkTable('wfPendingIssues'));
$fetchCoreHashesStatus = wfIssues::statusStart(__("Fetching core, theme and plugin file signatures from Wordfence", 'wordfence'));
$this->knownFiles = $this->engine->getKnownFilesLoader()->getKnownFiles();
} catch (wfScanKnownFilesException $e) {
wfIssues::statusEndErr();
wfIssues::statusEnd($fetchCoreHashesStatus, wfIssues::STATUS_SUCCESS);
if ($this->malwareEnabled) {
$malwarePrefixStatus = wfIssues::statusStart(__("Fetching list of known malware files from Wordfence", 'wordfence'));
$stored = wfConfig::get_ser('malwarePrefixes', array(), false);
if (is_array($stored) && isset($stored['hash']) && $stored['hash'] == $malwarePrefixesHash && isset($stored['prefixes']) && wfWAFUtils::strlen($stored['prefixes']) % 4 == 0) {
wordfence::status(4, 'info', __("Using cached malware prefixes", 'wordfence'));
wordfence::status(4, 'info', __("Fetching fresh malware prefixes", 'wordfence'));
$malwareData = $engine->api->getStaticURL('/malwarePrefixes.bin');
wfIssues::statusEndErr();
throw new Exception(__("Could not fetch malware signatures from Wordfence servers.", 'wordfence'));
if (wfWAFUtils::strlen($malwareData) % 4 != 0) {
wfIssues::statusEndErr();
throw new Exception(__("Malware data received from Wordfence servers was not valid.", 'wordfence'));
$stored = array('hash' => $malwarePrefixesHash, 'prefixes' => $malwareData);
wfConfig::set_ser('malwarePrefixes', $stored, true, wfConfig::DONT_AUTOLOAD);
$this->malwareData = $stored['prefixes'];
wfIssues::statusEnd($malwarePrefixStatus, wfIssues::STATUS_SUCCESS);
if ($this->coreUnknownEnabled) {
$coreHashesStatus = wfIssues::statusStart(__("Fetching list of known core files from Wordfence", 'wordfence'));
$stored = wfConfig::get_ser('coreHashes', array(), false);
if (is_array($stored) && isset($stored['hash']) && $stored['hash'] == $coreHashesHash && isset($stored['hashes']) && wfWAFUtils::strlen($stored['hashes']) > 0 && wfWAFUtils::strlen($stored['hashes']) % 32 == 0) {
wordfence::status(4, 'info', __("Using cached core hashes", 'wordfence'));
wordfence::status(4, 'info', __("Fetching fresh core hashes", 'wordfence'));
$coreHashesData = $engine->api->getStaticURL('/coreHashes.bin');
wfIssues::statusEndErr();
throw new Exception(__("Could not fetch core hashes from Wordfence servers.", 'wordfence'));
if (wfWAFUtils::strlen($coreHashesData) % 32 != 0) {
wfIssues::statusEndErr();
throw new Exception(__("Core hashes data received from Wordfence servers was not valid.", 'wordfence'));
$stored = array('hash' => $coreHashesHash, 'hashes' => $coreHashesData);
wfConfig::set_ser('coreHashes', $stored, true, wfConfig::DONT_AUTOLOAD);
$this->coreHashesData = $stored['hashes'];
wfIssues::statusEnd($coreHashesStatus, wfIssues::STATUS_SUCCESS);
$this->haveIssues = array(
'core' => wfIssues::STATUS_SECURE,
'coreUnknown' => wfIssues::STATUS_SECURE,
'themes' => wfIssues::STATUS_SECURE,
'plugins' => wfIssues::STATUS_SECURE,
'malware' => wfIssues::STATUS_SECURE,
if($this->coreEnabled){ $this->status['core'] = wfIssues::statusStart(__("Comparing core WordPress files against originals in repository", 'wordfence')); $this->engine->scanController()->startStage(wfScanner::STAGE_FILE_CHANGES); } else { wfIssues::statusDisabled(__("Skipping core scan", 'wordfence')); }
if($this->themesEnabled){ $this->status['themes'] = wfIssues::statusStart(__("Comparing open source themes against WordPress.org originals", 'wordfence')); $this->engine->scanController()->startStage(wfScanner::STAGE_FILE_CHANGES); } else { wfIssues::statusDisabled(__("Skipping theme scan", 'wordfence')); }
if($this->pluginsEnabled){ $this->status['plugins'] = wfIssues::statusStart(__("Comparing plugins against WordPress.org originals", 'wordfence')); $this->engine->scanController()->startStage(wfScanner::STAGE_FILE_CHANGES); } else { wfIssues::statusDisabled(__("Skipping plugin scan", 'wordfence')); }
if($this->malwareEnabled){ $this->status['malware'] = wfIssues::statusStart(__("Scanning for known malware files", 'wordfence')); $this->engine->scanController()->startStage(wfScanner::STAGE_MALWARE_SCAN); } else { wfIssues::statusDisabled(__("Skipping malware scan", 'wordfence')); }
if($this->coreUnknownEnabled){ $this->status['coreUnknown'] = wfIssues::statusStart(__("Scanning for unknown files in wp-admin and wp-includes", 'wordfence')); $this->engine->scanController()->startStage(wfScanner::STAGE_FILE_CHANGES); } else { wfIssues::statusDisabled(__("Skipping unknown core file scan", 'wordfence')); }
if ($options['scansEnabled_fileContents']) { $this->engine->scanController()->startStage(wfScanner::STAGE_MALWARE_SCAN); }
if ($options['scansEnabled_fileContentsGSB']) { $this->engine->scanController()->startStage(wfScanner::STAGE_CONTENT_SAFETY); }
if ($this->coreUnknownEnabled && !$this->alertedOnUnknownWordPressVersion && empty($this->knownFiles['core'])) {
require(ABSPATH . 'wp-includes/version.php'); /* @var string $wp_version */
$this->alertedOnUnknownWordPressVersion = true;
$added = $this->engine->addIssue(
wfIssues::SEVERITY_MEDIUM,
'coreUnknown' . $wp_version,
'coreUnknown' . $wp_version,
sprintf(/* translators: WordPress version. */ __('Unknown WordPress core version: %s', 'wordfence'), $wp_version),
__("The core files scan will not be run because this version of WordPress is not currently indexed by Wordfence. This may be due to using a prerelease version or because the servers are still indexing a new release. If you are using an official WordPress release, this issue will automatically dismiss once the version is indexed and another scan is run.", 'wordfence'),
if ($added == wfIssues::ISSUE_ADDED || $added == wfIssues::ISSUE_UPDATED) { $this->haveIssues['coreUnknown'] = wfIssues::STATUS_PROBLEM; }
else if ($this->haveIssues['coreUnknown'] != wfIssues::STATUS_PROBLEM && ($added == wfIssues::ISSUE_IGNOREP || $added == wfIssues::ISSUE_IGNOREC)) { $this->haveIssues['coreUnknown'] = wfIssues::STATUS_IGNORED; }
$this->initializeProperties();
private function initializeProperties() {
$this->scanFileLogger = $this->getScanFileLogger();
$this->knownFileExclude = wordfenceScanner::getExcludeFilePattern(wordfenceScanner::EXCLUSION_PATTERNS_KNOWN_FILES);
public function __sleep(){
return array('totalFiles', 'totalDirs', 'totalData', 'stoppedOnFile', 'coreEnabled', 'pluginsEnabled', 'themesEnabled', 'malwareEnabled', 'coreUnknownEnabled', 'knownFiles', 'haveIssues', 'status', 'possibleMalware', 'scannedFiles', 'totalForks', 'alertedOnUnknownWordPressVersion', 'foldersProcessed', 'suspectedFiles', 'indexed', 'indexSize', 'currentIndex', 'coalescingIssues', 'pathMap');
public function __wakeup(){
$this->startTime = microtime(true);
$stored = wfConfig::get_ser('malwarePrefixes', array(), false);
if (!isset($stored['prefixes'])) {
$stored['prefixes'] = '';
$this->malwareData = $stored['prefixes'];
$stored = wfConfig::get_ser('coreHashes', array(), false);
if (!isset($stored['hashes'])) {
$this->coreHashesData = $stored['hashes'];
$this->initializeProperties();
public function getSuspectedFiles() {
return array_keys($this->suspectedFiles);
public function run($engine) {
if($this->totalForks > 1000){
throw new Exception(sprintf(/* translators: File path. */ __("Wordfence file scanner detected a possible infinite loop. Exiting on file: %s", 'wordfence'), $this->stoppedOnFile));
$start = microtime(true);
foreach ($this->scannedFiles as $file) {
$this->_dirIndex($file, $indexedFiles);
$this->_serviceIndexQueue($indexedFiles, true);
unset($this->foldersEntered); $this->foldersEntered = array();
unset($this->foldersProcessed); $this->foldersProcessed = array();
wordfence::status(4, 'info', sprintf(/* translators: Time in seconds. */ __("Index time: %s", 'wordfence'), ($end - $start)));
$this->_checkForTimeout();
wordfence::status(4, 'info', __("Beginning file hashing", 'wordfence'));
while ($file = $this->_nextFile()) {
$this->processFile($file);
wfUtils::afterProcessingFile();
$this->_checkForTimeout($file);
$this->processFileRecords(); // Ensure all file records have actually been inserted before processing pending issues
wordfence::status(4, 'info', __("Processing pending issues", 'wordfence'));
$this->_processPendingIssues();
wordfence::status(2, 'info', sprintf(/* translators: 1. Number of files. 2. Data in bytes. */ __('Analyzed %1$d files containing %2$s of data.', 'wordfence'), $this->totalFiles, wfUtils::formatBytes($this->totalData)));
if($this->coreEnabled){ wfIssues::statusEnd($this->status['core'], $this->haveIssues['core']); $this->engine->scanController()->completeStage(wfScanner::STAGE_FILE_CHANGES, $this->haveIssues['core']); }
if($this->themesEnabled){ wfIssues::statusEnd($this->status['themes'], $this->haveIssues['themes']); $this->engine->scanController()->completeStage(wfScanner::STAGE_FILE_CHANGES, $this->haveIssues['themes']); }
if($this->pluginsEnabled){ wfIssues::statusEnd($this->status['plugins'], $this->haveIssues['plugins']); $this->engine->scanController()->completeStage(wfScanner::STAGE_FILE_CHANGES, $this->haveIssues['plugins']); }
if($this->coreUnknownEnabled){ wfIssues::statusEnd($this->status['coreUnknown'], $this->haveIssues['coreUnknown']); $this->engine->scanController()->completeStage(wfScanner::STAGE_FILE_CHANGES, $this->haveIssues['coreUnknown']); }
if(sizeof($this->possibleMalware) > 0){
$malwareResp = $engine->api->binCall('check_possible_malware', json_encode($this->possibleMalware));
if($malwareResp['code'] != 200){
wfIssues::statusEndErr();
throw new Exception(__("Invalid response from Wordfence API during check_possible_malware", 'wordfence'));
$malwareList = json_decode($malwareResp['data'], true);
if(is_array($malwareList) && sizeof($malwareList) > 0){
for($i = 0; $i < sizeof($malwareList); $i++){
$file = $malwareList[$i][0];
$md5 = $malwareList[$i][1];
$name = $malwareList[$i][2];
$added = $this->engine->addIssue(
wfIssues::SEVERITY_CRITICAL,
sprintf(/* translators: File path. */ __('This file is suspected malware: %s', 'wordfence'), $file),
sprintf(/* translators: Malware name/title. */ __("This file's signature matches a known malware file. The title of the malware is '%s'. Immediately inspect this file using the 'View' option below and consider deleting it from your server.", 'wordfence'), $name),
'realFile' => array_key_exists($file, $this->pathMap) ? $this->pathMap[$file] : null,
if ($added == wfIssues::ISSUE_ADDED || $added == wfIssues::ISSUE_UPDATED) { $this->haveIssues['malware'] = wfIssues::STATUS_PROBLEM; }
else if ($this->haveIssues['malware'] != wfIssues::STATUS_PROBLEM && ($added == wfIssues::ISSUE_IGNOREP || $added == wfIssues::ISSUE_IGNOREC)) { $this->haveIssues['malware'] = wfIssues::STATUS_IGNORED; }
if($this->malwareEnabled){ wfIssues::statusEnd($this->status['malware'], $this->haveIssues['malware']); $this->engine->scanController()->completeStage(wfScanner::STAGE_MALWARE_SCAN, $this->haveIssues['malware']); }
unset($this->knownFiles); $this->knownFiles = false;
private function _dirIndex($file, &$indexedFiles) {
$realPath = $file->getRealPath();
//Applies to files and dirs
if (!is_readable($realPath))
if (!$this->_shouldProcessFile($file))
if (isset($this->foldersEntered[$realPath]))
$this->foldersEntered[$file->getRealPath()] = 1;
foreach (wfFileUtils::getContents($realPath) as $child) {
if (wfFileUtils::isCurrentOrParentDirectory($child)) {
$child = $file->createChild($child);
catch (wfInvalidPathException $e) {
wordfence::status(4, 'info', sprintf(__("Ignoring invalid scan file child: %s", 'wordfence'), $e->getPath()));
if (is_file($child->getRealPath())) {
$relativeFile = $child->getWordpressPath();
if ($this->stoppedOnFile && $child->getRealPath() != $this->stoppedOnFile) {
if (preg_match('/\.suspected$/i', $relativeFile)) { //Already iterating over all files in the search areas so generate this list here
wordfence::status(4, 'info', sprintf(/* translators: File path. */ __("Found .suspected file: %s", 'wordfence'), $relativeFile));
$this->suspectedFiles[$relativeFile] = 1;
$this->_checkForTimeout($child, $indexedFiles);
if ($this->_shouldHashFile($child)) {
$indexedFiles[] = $child;
wordfence::status(4, 'info', sprintf(/* translators: File path. */ __("Skipping unneeded hash: %s", 'wordfence'), (string) $child));
$this->_serviceIndexQueue($indexedFiles);
elseif (is_dir($child->getRealPath())) {
$this->_dirIndex($child, $indexedFiles);
catch (wfInaccessibleDirectoryException $e) {
wordfence::status(4, 'info', sprintf(/* translators: File path. */ __("Skipping inaccessible directory: %s", 'wordfence'), (string) $file));
$this->foldersProcessed[$realPath] = 1;
unset($this->foldersEntered[$realPath]);
if (is_file($realPath)) {
$relativeFile = $file->getWordpressPath();
if ($this->stoppedOnFile && $realPath != $this->stoppedOnFile) {
if (preg_match('/\.suspected$/i', $relativeFile)) { //Already iterating over all files in the search areas so generate this list here
wordfence::status(4, 'info', sprintf(/* translators: File path. */ __("Found .suspected file: %s", 'wordfence'), $relativeFile));
$this->suspectedFiles[$relativeFile] = 1;
$this->_checkForTimeout($file, $indexedFiles);
if ($this->_shouldHashFile($file)) {
wordfence::status(4, 'info', sprintf(/* translators: File path. */ __("Skipping unneeded hash: %s", 'wordfence'), (string) $file));
$this->_serviceIndexQueue($indexedFiles);
private function _serviceIndexQueue(&$indexedFiles, $final = false) {
if (count($indexedFiles) > 500) {
$files = array_splice($indexedFiles, 0, 500);
$fileCount = count($files);
foreach ($files as $file) {
$payload[] = (string) $file;
$payload[] = $file->getWordpressPath();
$table_wfKnownFileList = wfDB::networkTable('wfKnownFileList');
$query = substr("INSERT INTO {$table_wfKnownFileList} (path, wordpress_path) VALUES " . str_repeat("('%s', '%s'), ", count($files)), 0, -2);
$wpdb->query($wpdb->prepare($query, $payload));
$this->indexSize += $fileCount;
wordfence::status(2, 'info', sprintf(/* translators: Number of files. */ __("%d files indexed", 'wordfence'), $this->indexSize));
private function _loadFileBatch() {
$table_wfKnownFileList = wfDB::networkTable('wfKnownFileList');
$rows = $wpdb->get_results($wpdb->prepare("SELECT id, path, wordpress_path FROM {$table_wfKnownFileList} WHERE id > %d ORDER BY id ASC LIMIT 500", $this->currentIndex));
while (($row = prev($rows)) !== false) {
$this->currentFile = new wfScanFileListItem($row->id, $row->path, $row->wordpress_path, $this->currentFile);
private function _nextFile() {
if ($this->currentFile !== null)
$this->currentFile = $this->currentFile->getNext();
if ($this->currentFile === null) {
if ($this->currentFile !== null)
$this->currentIndex = $this->currentFile->getId();
return $this->currentFile;
private function _checkForTimeout($file = null, $indexQueue = false) {
$realPath = $file ? $file->getRealPath() : null;
if (($this->stoppedOnFile !== $realPath) && $this->engine->shouldFork()) { //max X seconds but don't allow fork if we're looking for the file we stopped on. Search mode is VERY fast.
$this->processFileRecords(false);
if ($indexQueue !== false) {
$this->_serviceIndexQueue($indexQueue, true);
$this->stoppedOnFile = $realPath;
wordfence::status(4, 'info', sprintf(/* translators: File path. */ __("Forking during indexing: %s", 'wordfence'), (string) $file));
wordfence::status(4, 'info', sprintf(/* translators: PHP max execution time. */ __("Calling fork() from wordfenceHash with maxExecTime: %s", 'wordfence'), $this->engine->maxExecTime));
if ($this->stoppedOnFile && $realPath != $this->stoppedOnFile && $indexQueue !== false) {
else if ($this->stoppedOnFile && $realPath == $this->stoppedOnFile) {
$this->stoppedOnFile = false; //Continue indexing
private function _shouldProcessFile($file) {
$excludePatterns = wordfenceScanner::getExcludeFilePattern(wordfenceScanner::EXCLUSION_PATTERNS_USER);
foreach ($excludePatterns as $pattern) {
if (preg_match($pattern, $file->getWordpressPath())) {
$realPath = $file->getRealPath();
if (isset($this->foldersProcessed[$realPath])) {
private function getScanFileLogger() {
if (function_exists('memory_get_usage')) {
return function($realPath) {
wordfence::status(4, 'info', sprintf(/* translators: 1. File path. 2. Memory in bytes. */ __('Scanning: %1$s (Mem:%2$s)', 'wordfence'), $realPath, sprintf('%.1fM', memory_get_usage(true) / (1024 * 1024))));
return function($realPath) {
wordfence::status(4, 'info', sprintf(/* translators: File path. */ __("Scanning: %s", 'wordfence'), $realPath));
private function isKnownFileScanAllowed($realPath) {
if ($this->knownFileExclude) {
foreach ($this->knownFileExclude as $pattern) {
if (preg_match($pattern, $realPath)) {
private function getKnownFileType($properties) {
if ($this->isKnownFileScanAllowed($properties->realPath)) {
foreach (self::$KNOWN_FILE_TYPES as $type) {
if (isset($this->knownFiles[$type][$properties->wordpressPath]))
if ($this->coreUnknownEnabled && !$this->alertedOnUnknownWordPressVersion)
return self::KNOWN_FILE_OTHER;
private function checkKnownCoreFile($properties) {
if (strtoupper($this->knownFiles['core'][$properties->wordpressPath]) == $properties->shac) {
$properties->freeContent();
if ($this->coreEnabled) {
if ($properties->loadContent() && (!preg_match('/<\?' . 'php[\r\n\s\t]*\/\/[\r\n\s\t]*Silence is golden\.[\r\n\s\t]*(?:\?>)?[\r\n\s\t]*$/s', $properties->content))) {
$this->engine->addPendingIssue(
'coreModified' . $properties->wordpressPath,
'coreModified' . $properties->wordpressPath . $properties->md5,
sprintf(/* translators: File path. */ __('WordPress core file modified: %s', 'wordfence'), $properties->wordpressPath),
__("This WordPress core file has been modified and differs from the original file distributed with this version of WordPress.", 'wordfence'),