Wordfence Security – Firewall & Malware Scan - Version 6.2.4

Version Description

  • Improvement: Scan times for very large sites with huge numbers of files are greatly improved.
  • Improvement: Added a configurable time limit for scans to help reduce overall server load and identify configuration problems.
  • Improvement: Email-based logins are now covered by "Don't let WordPress reveal valid users in login errors".
  • Improvement: Extended rate limiting support to the login page.
  • Fix: Fixed a case where files in the site root with issues could have them added multiple times.
  • Fix: Improved IP detection in the WAF when using an IP detection method that can have multiple values.
  • Fix: Added a safety check for when the database fails to return its max_allowed_packet value.
  • Fix: Added safety checks for when the configuration table migration has failed.
  • Fix: Added a couple rare failed login error codes to brute force detection.
  • Fix: Fixed a sequencing problem when adding detection for bot/human that led to it being called on every request.
  • Fix: Suppressed errors if a file is removed between the start of a scan and later scan stages.
  • Fix: Addressed a problem where the scan exclusions list was not checked correctly in some situations.
Download this release

Release Info

Developer wfryan
Plugin Icon 128x128 Wordfence Security – Firewall & Malware Scan
Version 6.2.4
Comparing to
See all releases

Code changes from version 6.2.3 to 6.2.4

js/admin.js CHANGED
@@ -179,8 +179,8 @@
179
}
180
startTicker = true;
181
if (this.needTour()) {
182
- this.tour('wfWelcomeContent3', 'wfHeading', 'top', 'left', "Learn about Site Performance", function() {
183
- self.tourRedir('WordfenceSitePerf');
184
});
185
}
186
} else if (jQuery('#wordfenceMode_options').length > 0) {
@@ -552,7 +552,7 @@
552
}
553
html += '">[' + item.date + ']&nbsp;' + item.msg + '</div>';
554
jQuery('#consoleActivity').append(html);
555
- if (/Scan complete\./i.test(item.msg)) {
556
this.loadIssues();
557
}
558
}
179
}
180
startTicker = true;
181
if (this.needTour()) {
182
+ this.tour('wfWelcomeContent3', 'wfHeading', 'top', 'left', "Learn about IP Blocking", function() {
183
+ self.tourRedir('WordfenceBlockedIPs');
184
});
185
}
186
} else if (jQuery('#wordfenceMode_options').length > 0) {
552
}
553
html += '">[' + item.date + ']&nbsp;' + item.msg + '</div>';
554
jQuery('#consoleActivity').append(html);
555
+ if (/Scan complete\./i.test(item.msg) || /Scan interrupted\./i.test(item.msg)) {
556
this.loadIssues();
557
}
558
}
lib/email_newIssues.php CHANGED
@@ -11,6 +11,12 @@
11
</div>
12
<?php endif ?>
13
14
<?php if($totalCriticalIssues > 0){ ?>
15
<p>Critical Problems:</p>
16
11
</div>
12
<?php endif ?>
13
14
+ <?php if ($timeLimitReached): ?>
15
+ <div style="margin: 12px 0;padding: 8px; background-color: #ffffe0; border: 1px solid #ffd975; border-width: 1px 1px 1px 10px;">
16
+ <em>The scan was terminated early because it reached the time limit for scans. If you would like to allow your scans to run longer, you can customize the limit on the options page: <a href="<?php echo $adminURL; ?>admin.php?page=WordfenceSecOpt"><?php echo $adminURL; ?>admin.php?page=WordfenceSecOpt</a> or read more about scan options to improve scan speed here: <a href="https://docs.wordfence.com/en/Scan_time_limit">https://docs.wordfence.com/en/Scan_time_limit</a></em>
17
+ </div>
18
+ <?php endif ?>
19
+
20
<?php if($totalCriticalIssues > 0){ ?>
21
<p>Critical Problems:</p>
22
lib/menu_options.php CHANGED
@@ -632,6 +632,15 @@ $w = new wfConfig();
632
issues will be sent.
633
</td>
634
</tr>
635
<tr>
636
<td colspan="2">
637
<div class="wfMarker" id="wfMarkerFirewallRules"></div>
632
issues will be sent.
633
</td>
634
</tr>
635
+ <tr>
636
+ <th>Time limit that a scan can run in seconds.<a
637
+ href="http://docs.wordfence.com/en/Wordfence_options#Time_limit_that_a_scan_can_run_in_seconds"
638
+ target="_blank" class="wfhelp"></a></th></th>
639
+ <td>
640
+ <input type="text" name="scan_maxDuration" id="scan_maxDuration"
641
+ value="<?php $w->f( 'scan_maxDuration' ); ?>"/> 0 or empty means the default of <?php echo wfUtils::makeDuration(WORDFENCE_DEFAULT_MAX_SCAN_TIME); ?> will be used.
642
+ </td>
643
+ </tr>
644
<tr>
645
<td colspan="2">
646
<div class="wfMarker" id="wfMarkerFirewallRules"></div>
lib/menu_scan.php CHANGED
@@ -1134,6 +1134,37 @@ $sigUpdateTime = wfConfig::get('signatureUpdateTime');
1134
</div>
1135
</script>
1136
1137
1138
1139
@@ -1161,7 +1192,7 @@ $sigUpdateTime = wfConfig::get('signatureUpdateTime');
1161
<div>
1162
<h3>Welcome to Wordfence</h3>
1163
<p>
1164
- Wordfence is a robust and complete security system and performance enhancer for WordPress. It protects your WordPress site
1165
from security threats and keeps you off Google's SEO black-list by providing a firewall, brute force protection, continuous scanning and many other security enhancements.
1166
</p>
1167
<p>
1134
</div>
1135
</script>
1136
1137
+ <script type="text/x-jquery-template" id="issueTmpl_timelimit">
1138
+ <div>
1139
+ <div class="wfIssue">
1140
+ <h2>${shortMsg}</h2>
1141
+ <p>
1142
+ <table border="0" class="wfIssue" cellspacing="0" cellpadding="0">
1143
+ <tr><th>Severity:</th><td>{{if severity == '1'}}Critical{{else}}Warning{{/if}}</td></tr>
1144
+ <tr><th>Status</th><td>
1145
+ {{if status == 'new' }}New{{/if}}
1146
+ {{if status == 'ignoreC' }}This issue will be ignored until it changes.{{/if}}
1147
+ {{if status == 'ignoreP' }}This issue is permanently ignored.{{/if}}
1148
+ </td></tr>
1149
+ </table>
1150
+ </p>
1151
+ <p>
1152
+ {{html longMsg}}
1153
+ </p>
1154
+ <div class="wfIssueOptions">
1155
+ {{if status == 'new'}}
1156
+ <strong>Resolve:</strong>
1157
+ <a href="#" onclick="WFAD.updateIssueStatus('${id}', 'delete'); return false;">I have fixed this issue</a>
1158
+ <a href="#" onclick="WFAD.updateIssueStatus('${id}', 'ignoreP'); return false;">Ignore this problem</a>
1159
+ {{/if}}
1160
+ {{if status == 'ignoreP' || status == 'ignoreC'}}
1161
+ <a href="#" onclick="WFAD.updateIssueStatus('${id}', 'delete'); return false;">Stop ignoring this issue</a>
1162
+ {{/if}}
1163
+ </div>
1164
+ </div>
1165
+ </div>
1166
+ </script>
1167
+
1168
1169
1170
1192
<div>
1193
<h3>Welcome to Wordfence</h3>
1194
<p>
1195
+ Wordfence is a robust and complete security system for WordPress. It protects your WordPress site
1196
from security threats and keeps you off Google's SEO black-list by providing a firewall, brute force protection, continuous scanning and many other security enhancements.
1197
</p>
1198
<p>
lib/wfConfig.php CHANGED
@@ -92,7 +92,7 @@ class wfConfig {
92
"otherParams" => array(
93
"scan_include_extra" => "",
94
// 'securityLevel' => '2',
95
- "alertEmails" => "", "liveTraf_ignoreUsers" => "", "liveTraf_ignoreIPs" => "", "liveTraf_ignoreUA" => "", "apiKey" => "", "maxMem" => '256', 'scan_exclude' => '', 'scan_maxIssues' => 1000, 'whitelisted' => '', 'bannedURLs' => '', 'maxExecutionTime' => '', 'howGetIPs' => '', 'actUpdateInterval' => '', 'alert_maxHourly' => 0, 'loginSec_userBlacklist' => '',
96
'liveTraf_maxRows' => 2000,
97
"neverBlockBG" => "neverBlockVerified",
98
"loginSec_countFailMins" => "240",
@@ -429,6 +429,7 @@ class wfConfig {
429
430
global $wpdb;
431
$dbh = $wpdb->dbh;
432
433
if (!self::$tableExists) {
434
return;
@@ -443,21 +444,26 @@ class wfConfig {
443
$data = serialize($val);
444
}
445
446
- if (!$wpdb->use_mysqli) {
447
$data = bin2hex($data);
448
}
449
450
$dataLength = strlen($data);
451
- $chunkSize = intval((self::getDB()->getMaxAllowedPacketBytes() - 50) / 1.2); //Based on max_allowed_packet + 20% for escaping and SQL
452
$chunkSize = $chunkSize - ($chunkSize % 2); //Ensure it's even
453
$chunkedValueKey = self::ser_chunked_key($key);
454
if ($dataLength > $chunkSize) {
455
$chunks = 0;
456
while (($chunks * $chunkSize) < $dataLength) {
457
$dataChunk = substr($data, $chunks * $chunkSize, $chunkSize);
458
- if ($wpdb->use_mysqli) {
459
$chunkKey = $chunkedValueKey . $chunks;
460
$stmt = $dbh->prepare("INSERT IGNORE INTO " . self::table() . " (name, val, autoload) VALUES (?, ?, 'no')");
461
$null = NULL;
462
$stmt->bind_param("sb", $chunkKey, $null);
463
@@ -489,14 +495,22 @@ class wfConfig {
489
else {
490
$exists = self::getDB()->querySingle("select name from " . self::table() . " where name='%s'", $key);
491
492
- if ($wpdb->use_mysqli) {
493
if ($exists) {
494
$stmt = $dbh->prepare("UPDATE " . self::table() . " SET val=? WHERE name=?");
495
$null = NULL;
496
$stmt->bind_param("bs", $null, $key);
497
}
498
else {
499
$stmt = $dbh->prepare("INSERT IGNORE INTO " . self::table() . " (val, name, autoload) VALUES (?, ?, ?)");
500
$null = NULL;
501
$stmt->bind_param("bss", $null, $key, $autoload);
502
}
92
"otherParams" => array(
93
"scan_include_extra" => "",
94
// 'securityLevel' => '2',
95
+ "alertEmails" => "", "liveTraf_ignoreUsers" => "", "liveTraf_ignoreIPs" => "", "liveTraf_ignoreUA" => "", "apiKey" => "", "maxMem" => '256', 'scan_exclude' => '', 'scan_maxIssues' => 1000, 'scan_maxDuration' => '', 'whitelisted' => '', 'bannedURLs' => '', 'maxExecutionTime' => '', 'howGetIPs' => '', 'actUpdateInterval' => '', 'alert_maxHourly' => 0, 'loginSec_userBlacklist' => '',
96
'liveTraf_maxRows' => 2000,
97
"neverBlockBG" => "neverBlockVerified",
98
"loginSec_countFailMins" => "240",
429
430
global $wpdb;
431
$dbh = $wpdb->dbh;
432
+ $useMySQLi = (is_object($dbh) && $wpdb->use_mysqli);
433
434
if (!self::$tableExists) {
435
return;
444
$data = serialize($val);
445
}
446
447
+ if (!$useMySQLi) {
448
$data = bin2hex($data);
449
}
450
451
$dataLength = strlen($data);
452
+ $maxAllowedPacketBytes = self::getDB()->getMaxAllowedPacketBytes();
453
+ $chunkSize = intval((($maxAllowedPacketBytes < 1024 /* MySQL minimum, probably failure to fetch it */ ? 1024 * 1024 /* MySQL default */ : $maxAllowedPacketBytes) - 50) / 1.2); //Based on max_allowed_packet + 20% for escaping and SQL
454
$chunkSize = $chunkSize - ($chunkSize % 2); //Ensure it's even
455
$chunkedValueKey = self::ser_chunked_key($key);
456
if ($dataLength > $chunkSize) {
457
$chunks = 0;
458
while (($chunks * $chunkSize) < $dataLength) {
459
$dataChunk = substr($data, $chunks * $chunkSize, $chunkSize);
460
+ if ($useMySQLi) {
461
$chunkKey = $chunkedValueKey . $chunks;
462
$stmt = $dbh->prepare("INSERT IGNORE INTO " . self::table() . " (name, val, autoload) VALUES (?, ?, 'no')");
463
+ if ($stmt === false) {
464
+ wordfence::status(2, 'error', "Error writing value chunk for {$key} (MySQLi error: [{$dbh->errno}] {$dbh->error})");
465
+ return false;
466
+ }
467
$null = NULL;
468
$stmt->bind_param("sb", $chunkKey, $null);
469
495
else {
496
$exists = self::getDB()->querySingle("select name from " . self::table() . " where name='%s'", $key);
497
498
+ if ($useMySQLi) {
499
if ($exists) {
500
$stmt = $dbh->prepare("UPDATE " . self::table() . " SET val=? WHERE name=?");
501
+ if ($stmt === false) {
502
+ wordfence::status(2, 'error', "Error writing value for {$key} (MySQLi error: [{$dbh->errno}] {$dbh->error})");
503
+ return false;
504
+ }
505
$null = NULL;
506
$stmt->bind_param("bs", $null, $key);
507
}
508
else {
509
$stmt = $dbh->prepare("INSERT IGNORE INTO " . self::table() . " (val, name, autoload) VALUES (?, ?, ?)");
510
+ if ($stmt === false) {
511
+ wordfence::status(2, 'error', "Error writing value for {$key} (MySQLi error: [{$dbh->errno}] {$dbh->error})");
512
+ return false;
513
+ }
514
$null = NULL;
515
$stmt->bind_param("bss", $null, $key, $autoload);
516
}
lib/wfDB.php CHANGED
@@ -84,7 +84,7 @@ class wfDB {
84
}
85
public function getMaxAllowedPacketBytes(){
86
$rec = $this->querySingleRec("show variables like 'max_allowed_packet'");
87
- return $rec['Value'];
88
}
89
public function getMaxLongDataSizeBytes() {
90
$rec = $this->querySingleRec("show variables like 'max_long_data_size'");
84
}
85
public function getMaxAllowedPacketBytes(){
86
$rec = $this->querySingleRec("show variables like 'max_allowed_packet'");
87
+ return intval($rec['Value']);
88
}
89
public function getMaxLongDataSizeBytes() {
90
$rec = $this->querySingleRec("show variables like 'max_long_data_size'");
lib/wfIssues.php CHANGED
@@ -84,7 +84,7 @@ class wfIssues {
84
public function ignoreAllNew(){
85
$this->getDB()->queryWrite("update " . $this->issuesTable . " set status='ignoreC' where status='new'");
86
}
87
- public function emailNewIssues(){
88
$level = wfConfig::getAlertLevel();
89
$emails = wfConfig::getAlertEmails();
90
$shortSiteURL = preg_replace('/^https?:\/\//i', '', site_url());
@@ -141,6 +141,7 @@ class wfIssues {
141
'level' => $level,
142
'issuesNotShown' => $overflowCount,
143
'adminURL' => get_admin_url(),
144
));
145
146
wp_mail(implode(',', $emails), $subject, $content, 'Content-type: text/html');
84
public function ignoreAllNew(){
85
$this->getDB()->queryWrite("update " . $this->issuesTable . " set status='ignoreC' where status='new'");
86
}
87
+ public function emailNewIssues($timeLimitReached = false){
88
$level = wfConfig::getAlertLevel();
89
$emails = wfConfig::getAlertEmails();
90
$shortSiteURL = preg_replace('/^https?:\/\//i', '', site_url());
141
'level' => $level,
142
'issuesNotShown' => $overflowCount,
143
'adminURL' => get_admin_url(),
144
+ 'timeLimitReached' => $timeLimitReached,
145
));
146
147
wp_mail(implode(',', $emails), $subject, $content, 'Content-type: text/html');
lib/wfScan.php CHANGED
@@ -89,7 +89,15 @@ class wfScan {
89
}
90
try {
91
$scan->go();
92
- } catch (Exception $e){
93
wfUtils::clearScanLock();
94
self::status(2, 'error', "Scan terminated with error: " . $e->getMessage());
95
self::status(10, 'info', "SUM_KILLED:Previous scan terminated with an error. See below.");
89
}
90
try {
91
$scan->go();
92
+ }
93
+ catch (wfScanEngineDurationLimitException $e) {
94
+ wfUtils::clearScanLock();
95
+ self::logPeakMemory();
96
+ self::status(2, 'info', "Wordfence used " . sprintf('%.2f', (wfConfig::get('wfPeakMemory') - self::$peakMemAtStart) / 1024 / 1024) . "MB of memory for scan. Server peak memory usage was: " . sprintf('%.2f', wfConfig::get('wfPeakMemory') / 1024 / 1024) . "MB");
97
+ self::status(2, 'error', "Scan terminated with error: " . $e->getMessage());
98
+ exit();
99
+ }
100
+ catch (Exception $e){
101
wfUtils::clearScanLock();
102
self::status(2, 'error', "Scan terminated with error: " . $e->getMessage());
103
self::status(10, 'info', "SUM_KILLED:Previous scan terminated with an error. See below.");
lib/wfScanEngine.php CHANGED
@@ -130,7 +130,18 @@ class wfScanEngine {
130
$this->recordMetric('scan', 'duration', (time() - $this->startTime));
131
$this->recordMetric('scan', 'memory', wfConfig::get('wfPeakMemory', 0));
132
$this->submitMetrics();
133
- } catch(Exception $e){
134
wfConfig::set('lastScanCompleted', $e->getMessage());
135
$this->recordMetric('scan', 'duration', (time() - $this->startTime));
136
$this->recordMetric('scan', 'memory', wfConfig::get('wfPeakMemory', 0));
@@ -139,8 +150,29 @@ class wfScanEngine {
139
throw $e;
140
}
141
}
142
public function forkIfNeeded(){
143
self::checkForKill();
144
if(time() - $this->cycleStartTime > $this->maxExecTime){
145
wordfence::status(4, 'info', "Forking during hash scan to ensure continuity.");
146
$this->fork();
@@ -154,8 +186,8 @@ class wfScanEngine {
154
} //Otherwise there was an error so don't start another scan.
155
exit(0);
156
}
157
- public function emailNewIssues(){
158
- $this->i->emailNewIssues();
159
}
160
public function submitMetrics() {
161
if (wfConfig::get('other_WFNet', true)) {
@@ -461,18 +493,20 @@ class wfScanEngine {
461
if(! is_array($baseContents)){
462
throw new Exception("Wordfence could not read the contents of your base WordPress directory. This usually indicates your permissions are so strict that your web server can't read your WordPress directory.");
463
}
464
$scanOutside = wfConfig::get('other_scanOutside');
465
- if($scanOutside){
466
wordfence::status(2, 'info', "Including files that are outside the WordPress installation in the scan.");
467
}
468
- $includeInKnownFilesScan = array();
469
- foreach($baseContents as $file){ //Only include base files less than a meg that are files.
470
- if($file == '.' || $file == '..'){ continue; }
471
- $fullFile = rtrim(ABSPATH, '/') . '/' . $file;
472
- if($scanOutside){
473
- $includeInKnownFilesScan[] = $file;
474
- } else if(in_array($file, $baseWPStuff) || (@is_file($fullFile) && @is_readable($fullFile) && (! wfUtils::fileTooBig($fullFile)) ) ){
475
- $includeInKnownFilesScan[] = $file;
476
}
477
}
478
@@ -1163,7 +1197,7 @@ class wfScanEngine {
1163
return $this->i->addIssue($type, $severity, $ignoreP, $ignoreC, $shortMsg, $longMsg, $templateData);
1164
}
1165
public static function requestKill(){
1166
- wfConfig::set('wfKillRequested', time());
1167
}
1168
public static function checkForKill(){
1169
$kill = wfConfig::get('wfKillRequested', 0);
@@ -1175,7 +1209,7 @@ class wfScanEngine {
1175
public static function startScan($isFork = false){
1176
if(! $isFork){ //beginning of scan
1177
wfConfig::inc('totalScansRun');
1178
- wfConfig::set('wfKillRequested', 0);
1179
wordfence::status(4, 'info', "Entering start scan routine");
1180
if(wfUtils::isScanRunning()){
1181
wfUtils::getScanFileError();
@@ -1621,4 +1655,7 @@ class wfCommonBackupFileTest {
1621
1622
class wfPubliclyAccessibleFileTest extends wfCommonBackupFileTest {
1623
1624
}
130
$this->recordMetric('scan', 'duration', (time() - $this->startTime));
131
$this->recordMetric('scan', 'memory', wfConfig::get('wfPeakMemory', 0));
132
$this->submitMetrics();
133
+ }
134
+ catch (wfScanEngineDurationLimitException $e) {
135
+ wfConfig::set('lastScanCompleted', $e->getMessage());
136
+ $this->i->setScanTimeNow();
137
+
138
+ $this->emailNewIssues(true);
139
+ $this->recordMetric('scan', 'duration', (time() - $this->startTime));
140
+ $this->recordMetric('scan', 'memory', wfConfig::get('wfPeakMemory', 0));
141
+ $this->submitMetrics();
142
+ throw $e;
143
+ }
144
+ catch(Exception $e) {
145
wfConfig::set('lastScanCompleted', $e->getMessage());
146
$this->recordMetric('scan', 'duration', (time() - $this->startTime));
147
$this->recordMetric('scan', 'memory', wfConfig::get('wfPeakMemory', 0));
150
throw $e;
151
}
152
}
153
+ public function checkForDurationLimit() {
154
+ $timeLimit = intval(wfConfig::get('scan_maxDuration'));
155
+ if ($timeLimit < 1) {
156
+ $timeLimit = WORDFENCE_DEFAULT_MAX_SCAN_TIME;
157
+ }
158
+
159
+ if ((time() - $this->startTime) > $timeLimit){
160
+ $error = 'The scan time limit of ' . wfUtils::makeDuration($timeLimit) . ' has been exceeded and the scan will be terminated. This limit can be customized on the options page. <a href="http://docs.wordfence.com/en/Scan_time_limit" target="_blank">Get More Information</a>';
161
+ $this->addIssue('timelimit', 1, md5($this->startTime), md5($this->startTime), 'Scan Time Limit Exceeded', $error, array());
162
+ $summary = $this->i->getSummaryItems();
163
+ $this->status(1, 'info', '-------------------');
164
+ $this->status(1, 'info', "Scan interrupted. Scanned " . $summary['totalFiles'] . " files, " . $summary['totalPlugins'] . " plugins, " . $summary['totalThemes'] . " themes, " . ($summary['totalPages'] + $summary['totalPosts']) . " pages, " . $summary['totalComments'] . " comments and " . $summary['totalRows'] . " records in " . wfUtils::makeDuration(time() - $this->startTime, true) . ".");
165
+ if($this->i->totalIssues > 0){
166
+ $this->status(10, 'info', "SUM_FINAL:Scan interrupted. You have " . $this->i->totalIssues . " new issues to fix. See below.");
167
+ } else {
168
+ $this->status(10, 'info', "SUM_FINAL:Scan interrupted. No problems found prior to stopping.");
169
+ }
170
+ throw new wfScanEngineDurationLimitException($error);
171
+ }
172
+ }
173
public function forkIfNeeded(){
174
self::checkForKill();
175
+ $this->checkForDurationLimit();
176
if(time() - $this->cycleStartTime > $this->maxExecTime){
177
wordfence::status(4, 'info', "Forking during hash scan to ensure continuity.");
178
$this->fork();
186
} //Otherwise there was an error so don't start another scan.
187
exit(0);
188
}
189
+ public function emailNewIssues($timeLimitReached = false){
190
+ $this->i->emailNewIssues($timeLimitReached);
191
}
192
public function submitMetrics() {
193
if (wfConfig::get('other_WFNet', true)) {
493
if(! is_array($baseContents)){
494
throw new Exception("Wordfence could not read the contents of your base WordPress directory. This usually indicates your permissions are so strict that your web server can't read your WordPress directory.");
495
}
496
+
497
+ $includeInKnownFilesScan = array();
498
$scanOutside = wfConfig::get('other_scanOutside');
499
+ if ($scanOutside) {
500
wordfence::status(2, 'info', "Including files that are outside the WordPress installation in the scan.");
501
+ $includeInKnownFilesScan[] = ''; //Ends up as a literal ABSPATH
502
}
503
+ else {
504
+ foreach ($baseContents as $file) { //Only include base files less than a meg that are files.
505
+ if($file == '.' || $file == '..'){ continue; }
506
+ $fullFile = rtrim(ABSPATH, '/') . '/' . $file;
507
+ if (in_array($file, $baseWPStuff) || (@is_file($fullFile) && @is_readable($fullFile) && (!wfUtils::fileTooBig($fullFile)))) {
508
+ $includeInKnownFilesScan[] = $file;
509
+ }
510
}
511
}
512
1197
return $this->i->addIssue($type, $severity, $ignoreP, $ignoreC, $shortMsg, $longMsg, $templateData);
1198
}
1199
public static function requestKill(){
1200
+ wfConfig::set('wfKillRequested', time(), wfConfig::DONT_AUTOLOAD);
1201
}
1202
public static function checkForKill(){
1203
$kill = wfConfig::get('wfKillRequested', 0);
1209
public static function startScan($isFork = false){
1210
if(! $isFork){ //beginning of scan
1211
wfConfig::inc('totalScansRun');
1212
+ wfConfig::set('wfKillRequested', 0, wfConfig::DONT_AUTOLOAD);
1213
wordfence::status(4, 'info', "Entering start scan routine");
1214
if(wfUtils::isScanRunning()){
1215
wfUtils::getScanFileError();
1655
1656
class wfPubliclyAccessibleFileTest extends wfCommonBackupFileTest {
1657
1658
+ }
1659
+
1660
+ class wfScanEngineDurationLimitException extends Exception {
1661
}
lib/wfSchema.php CHANGED
@@ -24,8 +24,10 @@ class wfSchema {
24
KEY k1(wfsn)
25
) default charset=utf8",
26
"wfConfig" => "(
27
- name varchar(100) PRIMARY KEY NOT NULL,
28
- val longblob
29
) default charset=utf8",
30
"wfCrawlers" => "(
31
IP INT UNSIGNED NOT NULL,
@@ -177,6 +179,11 @@ class wfSchema {
177
KEY `expiration` (`expiration`),
178
KEY `IP` (`IP`)
179
) DEFAULT CHARSET=utf8;",
180
/*
181
'wfPerfLog' => "(
182
id int UNSIGNED NOT NULL auto_increment PRIMARY KEY,
24
KEY k1(wfsn)
25
) default charset=utf8",
26
"wfConfig" => "(
27
+ `name` varchar(100) NOT NULL,
28
+ `val` longblob,
29
+ `autoload` enum('no','yes') NOT NULL DEFAULT 'yes',
30
+ PRIMARY KEY (`name`)
31
) default charset=utf8",
32
"wfCrawlers" => "(
33
IP INT UNSIGNED NOT NULL,
179
KEY `expiration` (`expiration`),
180
KEY `IP` (`IP`)
181
) DEFAULT CHARSET=utf8;",
182
+ 'wfKnownFileList' => "(
183
+ `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
184
+ `path` text NOT NULL,
185
+ PRIMARY KEY (`id`)
186
+ ) DEFAULT CHARSET=utf8;",
187
/*
188
'wfPerfLog' => "(
189
id int UNSIGNED NOT NULL auto_increment PRIMARY KEY,
lib/wfUtils.php CHANGED
@@ -570,6 +570,7 @@ class wfUtils {
570
if(strpos($item, $char) !== false){
571
$sp = explode($char, $item);
572
foreach($sp as $j){
573
if (!self::isValidIP($j)) {
574
$j = preg_replace('/:\d+#x2F;', '', $j); //Strip off port
575
}
@@ -845,7 +846,7 @@ class wfUtils {
845
public static function getScanLock(){
846
//Windows does not support non-blocking flock, so we use time.
847
$scanRunning = wfConfig::get('wf_scanRunning');
848
- if($scanRunning && time() - $scanRunning < WORDFENCE_MAX_SCAN_TIME){
849
return false;
850
}
851
wfConfig::set('wf_scanRunning', time());
@@ -860,7 +861,7 @@ class wfUtils {
860
}
861
public static function isScanRunning(){
862
$scanRunning = wfConfig::get('wf_scanRunning');
863
- if($scanRunning && time() - $scanRunning < WORDFENCE_MAX_SCAN_TIME){
864
return true;
865
} else {
866
return false;
570
if(strpos($item, $char) !== false){
571
$sp = explode($char, $item);
572
foreach($sp as $j){
573
+ $j = trim($j);
574
if (!self::isValidIP($j)) {
575
$j = preg_replace('/:\d+#x2F;', '', $j); //Strip off port
576
}
846
public static function getScanLock(){
847
//Windows does not support non-blocking flock, so we use time.
848
$scanRunning = wfConfig::get('wf_scanRunning');
849
+ if($scanRunning && time() - $scanRunning < WORDFENCE_MAX_SCAN_LOCK_TIME){
850
return false;
851
}
852
wfConfig::set('wf_scanRunning', time());
861
}
862
public static function isScanRunning(){
863
$scanRunning = wfConfig::get('wf_scanRunning');
864
+ if($scanRunning && time() - $scanRunning < WORDFENCE_MAX_SCAN_LOCK_TIME){
865
return true;
866
} else {
867
return false;
lib/wordfenceClass.php CHANGED
@@ -1404,7 +1404,7 @@ SQL
1404
}
1405
else { //Code path for old method, invalid password the second time
1406
self::$authError = $authUser;
1407
- if (is_wp_error(self::$authError) && (self::$authError->get_error_code() == 'invalid_username' || self::$authError->get_error_code() == 'incorrect_password') && wfConfig::get('loginSec_maskLoginErrors')) {
1408
self::$authError = new WP_Error('incorrect_password', sprintf(__('<strong>ERROR</strong>: The username or password you entered is incorrect. <a href="%2$s" title="Password Lost and Found">Lost your password</a>?'), $username, wp_lostpassword_url()));
1409
}
1410
@@ -1694,7 +1694,7 @@ SQL
1694
if(self::getLog()->isWhitelisted($IP)){
1695
return $authUser;
1696
}
1697
- if(wfConfig::get('other_WFNet') && is_wp_error($authUser) && ($authUser->get_error_code() == 'invalid_username' || $authUser->get_error_code() == 'incorrect_password' || $authUser->get_error_code() == 'twofactor_invalid') ){
1698
if($maxBlockTime = self::wfsnIsBlocked($IP, 'brute')){
1699
self::getLog()->blockIP($IP, "Blocked by Wordfence Security Network", true, false, $maxBlockTime);
1700
$secsToGo = wfConfig::get('blockedTime');
@@ -1704,7 +1704,7 @@ SQL
1704
1705
}
1706
if($secEnabled){
1707
- if(is_wp_error($authUser) && $authUser->get_error_code() == 'invalid_username'){
1708
if($blacklist = wfConfig::get('loginSec_userBlacklist')){
1709
$users = explode("\n", wfUtils::cleanupOneEntryPerLine($blacklist));
1710
foreach($users as $user){
@@ -1725,7 +1725,7 @@ SQL
1725
}
1726
}
1727
$tKey = 'wflginfl_' . bin2hex(wfUtils::inet_pton($IP));
1728
- if(is_wp_error($authUser) && ($authUser->get_error_code() == 'invalid_username' || $authUser->get_error_code() == 'incorrect_password' || $authUser->get_error_code() == 'twofactor_invalid') ){
1729
$tries = get_transient($tKey);
1730
if($tries){
1731
$tries++;
@@ -1742,14 +1742,14 @@ SQL
1742
}
1743
}
1744
if(is_wp_error($authUser)){
1745
- if($authUser->get_error_code() == 'invalid_username'){
1746
self::getLog()->logLogin('loginFailInvalidUsername', 1, $username);
1747
} else {
1748
self::getLog()->logLogin('loginFailValidUsername', 1, $username);
1749
}
1750
}
1751
1752
- if(is_wp_error($authUser) && ($authUser->get_error_code() == 'invalid_username' || $authUser->get_error_code() == 'incorrect_password') && wfConfig::get('loginSec_maskLoginErrors')){
1753
return new WP_Error( 'incorrect_password', sprintf( __( '<strong>ERROR</strong>: The username or password you entered is incorrect. <a href="%2$s" title="Password Lost and Found">Lost your password</a>?' ), $username, wp_lostpassword_url() ) );
1754
}
1755
return $authUser;
@@ -1920,6 +1920,8 @@ SQL
1920
if(self::isLockedOut(wfUtils::getIP())){
1921
require('wfLockedOut.php');
1922
}
1923
}
1924
public static function authActionNew(&$username, &$passwd){ //As of php 5.4 we must denote passing by ref in the function definition, not the function call (as WordPress core does, which is a bug in WordPress).
1925
if(self::isLockedOut(wfUtils::getIP())){
@@ -4007,6 +4009,11 @@ wfscr.src = url;
4007
EOL;
4008
}
4009
public static function wfLogHumanHeader(){
4010
self::$hitID = self::getLog()->logHit();
4011
if (self::$hitID) {
4012
$URL = home_url('/?wordfence_logHuman=1&hid=' . wfUtils::encrypt(self::$hitID));
1404
}
1405
else { //Code path for old method, invalid password the second time
1406
self::$authError = $authUser;
1407
+ if (is_wp_error(self::$authError) && (self::$authError->get_error_code() == 'invalid_username' || $authUser->get_error_code() == 'invalid_email' || self::$authError->get_error_code() == 'incorrect_password' || $authUser->get_error_code() == 'authentication_failed') && wfConfig::get('loginSec_maskLoginErrors')) {
1408
self::$authError = new WP_Error('incorrect_password', sprintf(__('<strong>ERROR</strong>: The username or password you entered is incorrect. <a href="%2$s" title="Password Lost and Found">Lost your password</a>?'), $username, wp_lostpassword_url()));
1409
}
1410
1694
if(self::getLog()->isWhitelisted($IP)){
1695
return $authUser;
1696
}
1697
+ if(wfConfig::get('other_WFNet') && is_wp_error($authUser) && ($authUser->get_error_code() == 'invalid_username' || $authUser->get_error_code() == 'invalid_email' || $authUser->get_error_code() == 'incorrect_password' || $authUser->get_error_code() == 'twofactor_invalid' || $authUser->get_error_code() == 'authentication_failed') ){
1698
if($maxBlockTime = self::wfsnIsBlocked($IP, 'brute')){
1699
self::getLog()->blockIP($IP, "Blocked by Wordfence Security Network", true, false, $maxBlockTime);
1700
$secsToGo = wfConfig::get('blockedTime');
1704
1705
}
1706
if($secEnabled){
1707
+ if(is_wp_error($authUser) && ($authUser->get_error_code() == 'invalid_username') || ($authUser->get_error_code() == 'invalid_email')){
1708
if($blacklist = wfConfig::get('loginSec_userBlacklist')){
1709
$users = explode("\n", wfUtils::cleanupOneEntryPerLine($blacklist));
1710
foreach($users as $user){
1725
}
1726
}
1727
$tKey = 'wflginfl_' . bin2hex(wfUtils::inet_pton($IP));
1728
+ if(is_wp_error($authUser) && ($authUser->get_error_code() == 'invalid_username' || $authUser->get_error_code() == 'invalid_email' || $authUser->get_error_code() == 'incorrect_password' || $authUser->get_error_code() == 'twofactor_invalid' || $authUser->get_error_code() == 'authentication_failed') ){
1729
$tries = get_transient($tKey);
1730
if($tries){
1731
$tries++;
1742
}
1743
}
1744
if(is_wp_error($authUser)){
1745
+ if($authUser->get_error_code() == 'invalid_username' || $authUser->get_error_code() == 'invalid_email'){
1746
self::getLog()->logLogin('loginFailInvalidUsername', 1, $username);
1747
} else {
1748
self::getLog()->logLogin('loginFailValidUsername', 1, $username);
1749
}
1750
}
1751
1752
+ if(is_wp_error($authUser) && ($authUser->get_error_code() == 'invalid_username' || $authUser->get_error_code() == 'invalid_email' || $authUser->get_error_code() == 'incorrect_password') && wfConfig::get('loginSec_maskLoginErrors')){
1753
return new WP_Error( 'incorrect_password', sprintf( __( '<strong>ERROR</strong>: The username or password you entered is incorrect. <a href="%2$s" title="Password Lost and Found">Lost your password</a>?' ), $username, wp_lostpassword_url() ) );
1754
}
1755
return $authUser;
1920
if(self::isLockedOut(wfUtils::getIP())){
1921
require('wfLockedOut.php');
1922
}
1923
+
1924
+ self::doEarlyAccessLogging(); //Rate limiting
1925
}
1926
public static function authActionNew(&$username, &$passwd){ //As of php 5.4 we must denote passing by ref in the function definition, not the function call (as WordPress core does, which is a bug in WordPress).
1927
if(self::isLockedOut(wfUtils::getIP())){
4009
EOL;
4010
}
4011
public static function wfLogHumanHeader(){
4012
+ //Final check in case this was added as an action before the request was fully initialized
4013
+ if (self::getLog()->getCurrentRequest()->jsRun || !wfConfig::liveTrafficEnabled()) {
4014
+ return;
4015
+ }
4016
+
4017
self::$hitID = self::getLog()->logHit();
4018
if (self::$hitID) {
4019
$URL = home_url('/?wordfence_logHuman=1&hid=' . wfUtils::encrypt(self::$hitID));
lib/wordfenceConstants.php CHANGED
@@ -5,7 +5,8 @@ define('WORDFENCE_API_URL_NONSEC', 'http://noc1.wordfence.com/');
5
define('WORDFENCE_API_URL_BASE_SEC', WORDFENCE_API_URL_SEC . 'v' . WORDFENCE_API_VERSION . '/');
6
define('WORDFENCE_API_URL_BASE_NONSEC', WORDFENCE_API_URL_NONSEC . 'v' . WORDFENCE_API_VERSION . '/');
7
define('WORDFENCE_HACKATTEMPT_URL', 'http://noc3.wordfence.com/');
8
- define('WORDFENCE_MAX_SCAN_TIME', 86400); //Increased this from 10 mins to 1 day because very big scans run for a long time. Users can use kill.
9
define('WORDFENCE_TRANSIENTS_TIMEOUT', 3600); //how long are items cached in seconds e.g. files downloaded for diffing
10
define('WORDFENCE_MAX_IPLOC_AGE', 86400); //1 day
11
define('WORDFENCE_CRAWLER_VERIFY_CACHE_TIME', 604800);
5
define('WORDFENCE_API_URL_BASE_SEC', WORDFENCE_API_URL_SEC . 'v' . WORDFENCE_API_VERSION . '/');
6
define('WORDFENCE_API_URL_BASE_NONSEC', WORDFENCE_API_URL_NONSEC . 'v' . WORDFENCE_API_VERSION . '/');
7
define('WORDFENCE_HACKATTEMPT_URL', 'http://noc3.wordfence.com/');
8
+ define('WORDFENCE_MAX_SCAN_LOCK_TIME', 86400); //Increased this from 10 mins to 1 day because very big scans run for a long time. Users can use kill.
9
+ define('WORDFENCE_DEFAULT_MAX_SCAN_TIME', 10800);
10
define('WORDFENCE_TRANSIENTS_TIMEOUT', 3600); //how long are items cached in seconds e.g. files downloaded for diffing
11
define('WORDFENCE_MAX_IPLOC_AGE', 86400); //1 day
12
define('WORDFENCE_CRAWLER_VERIFY_CACHE_TIME', 604800);
lib/wordfenceHash.php CHANGED
@@ -25,9 +25,12 @@ class wordfenceHash {
25
private $only = false;
26
private $totalForks = 0;
27
private $alertedOnUnknownWordPressVersion = false;
28
private $foldersProcessed = array();
29
- private $filesProcessedBloomFilter = false;
30
private $suspectedFiles = array();
31
32
/**
33
* @param string $striplen
@@ -61,14 +64,13 @@ class wordfenceHash {
61
if(wfConfig::get('scansEnabled_coreUnknown')){
62
$this->coreUnknownEnabled = true;
63
}
64
-
65
- $this->filesProcessedBloomFilter = new wfMD5BloomFilter(65536, 8); //65536,8 produces the lowest miss probability. Uses approximately 8 KB of memory + PHP object overhead
66
67
$this->db = new wfDB();
68
69
//Doing a delete for now. Later we can optimize this to only scan modified files.
70
//$this->db->queryWrite("update " . $this->db->prefix() . "wfFileMods set oldMD5 = newMD5");
71
$this->db->truncate($this->db->prefix() . "wfFileMods");
72
$fetchCoreHashesStatus = wordfence::statusStart("Fetching core, theme and plugin file signatures from Wordfence");
73
try {
74
$this->knownFiles = $this->engine->getKnownFilesLoader()
@@ -116,7 +118,7 @@ class wordfenceHash {
116
if($this->coreUnknownEnabled){ $this->status['coreUnknown'] = wordfence::statusStart("Scanning for unknown files in wp-admin and wp-includes"); } else { wordfence::statusDisabled("Skipping unknown core file scan"); }
117
}
118
public function __sleep(){
119
- return array('striplen', 'totalFiles', 'totalDirs', 'totalData', 'stoppedOnFile', 'coreEnabled', 'pluginsEnabled', 'themesEnabled', 'malwareEnabled', 'coreUnknownEnabled', 'knownFiles', 'malwareData', 'haveIssues', 'status', 'possibleMalware', 'path', 'only', 'totalForks', 'alertedOnUnknownWordPressVersion', 'foldersProcessed', 'filesProcessedBloomFilter', 'suspectedFiles');
120
}
121
public function __wakeup(){
122
$this->db = new wfDB();
@@ -131,16 +133,37 @@ class wordfenceHash {
131
throw new Exception("Wordfence file scanner detected a possible infinite loop. Exiting on file: " . $this->stoppedOnFile);
132
}
133
$this->engine = $engine;
134
- $files = scandir($this->path);
135
- foreach($files as $file){
136
- if($file == '.' || $file == '..'){ continue; }
137
- if(sizeof($this->only) > 0 && (! in_array($file, $this->only))){
138
- continue;
139
}
140
- $file = $this->path . $file;
141
- wordfence::status(4, 'info', "Hashing item in base dir: $file");
142
- $this->_dirHash($file);
143
}
144
wordfence::status(2, 'info', "Analyzed " . $this->totalFiles . " files containing " . wfUtils::formatBytes($this->totalData) . " of data.");
145
if($this->coreEnabled){ wordfence::statusEnd($this->status['core'], $this->haveIssues['core']); }
146
if($this->themesEnabled){ wordfence::statusEnd($this->status['themes'], $this->haveIssues['themes']); }
@@ -179,53 +202,139 @@ class wordfenceHash {
179
}
180
if($this->malwareEnabled){ wordfence::statusEnd($this->status['malware'], $this->haveIssues['malware']); }
181
}
182
- private function _dirHash($path){
183
- if(substr($path, -3, 3) == '/..' || substr($path, -2, 2) == '/.'){
184
return;
185
}
186
- if(! is_readable($path)){ return; } //Applies to files and dirs
187
if (!$this->_shouldProcessPath($path)) { return; }
188
- $this->_checkForTimeout($path);
189
- if(is_dir($path)){
190
$this->totalDirs++;
191
- if($path[strlen($path) - 1] != '/'){
192
$path .= '/';
193
}
194
$cont = scandir($path);
195
- for($i = 0; $i < sizeof($cont); $i++){
196
- if($cont[$i] == '.' || $cont[$i] == '..'){ continue; }
197
$file = $path . $cont[$i];
198
- if(is_file($file)){
199
- $this->_checkForTimeout($file);
200
- $this->processFile($file);
201
- } else if(is_dir($file)) {
202
- $this->_dirHash($file);
203
}
204
}
205
206
- $realPath = realpath($path);
207
- $this->foldersProcessed[$realPath] = 1;
208
- } else {
209
- if(is_file($path)){
210
- $this->processFile($path);
211
}
212
}
213
}
214
- private function _checkForTimeout($path) {
215
$file = substr($path, $this->striplen);
216
if ((!$this->stoppedOnFile) && microtime(true) - $this->startTime > $this->engine->maxExecTime) { //max X seconds but don't allow fork if we're looking for the file we stopped on. Search mode is VERY fast.
217
- $this->stoppedOnFile = $file;
218
- wordfence::status(4, 'info', "Calling fork() from wordfenceHash::processFile with maxExecTime: " . $this->engine->maxExecTime);
219
$this->engine->fork();
220
//exits
221
}
222
223
- //Put this after the fork, that way we will at least scan one more file after we fork if it takes us more than 10 seconds to search for the stoppedOnFile
224
- if ($this->stoppedOnFile && $file != $this->stoppedOnFile) {
225
return;
226
}
227
else if ($this->stoppedOnFile && $file == $this->stoppedOnFile) {
228
- $this->stoppedOnFile = false; //Continue scanning
229
}
230
}
231
private function _shouldProcessPath($path) {
@@ -245,20 +354,11 @@ class wordfenceHash {
245
private function processFile($realFile){
246
$file = substr($realFile, $this->striplen);
247
248
- if (preg_match('/\.suspected#x2F;i', $file)) { //Already iterating over all files in the search areas so generate this list here
249
- wordfence::status(4, 'info', "Found .suspected file: $file");
250
- $this->suspectedFiles[$file] = 1;
251
- }
252
-
253
- if (!$this->_shouldHashFile($realFile)) {
254
- wordfence::status(4, 'info', "Skipping unneeded hash: {$realFile}");
255
- return;
256
- }
257
-
258
if(wfUtils::fileTooBig($realFile)){
259
wordfence::status(4, 'info', "Skipping file larger than max size: $realFile");
260
return;
261
}
262
if (function_exists('memory_get_usage')) {
263
wordfence::status(4, 'info', "Scanning: $realFile (Mem:" . sprintf('%.1f', memory_get_usage(true) / (1024 * 1024)) . "M)");
264
} else {
@@ -437,11 +537,9 @@ class wordfenceHash {
437
// we could split this into files who's path we recognize and file's who's path we recognize AND who have a valid sig.
438
// But because we want to scan files who's sig we don't recognize, regardless of known path or not, we only need one "knownFile" field.
439
$this->db->queryWrite("insert into " . $this->db->prefix() . "wfFileMods (filename, filenameMD5, knownFile, oldMD5, newMD5) values ('%s', unhex(md5('%s')), %d, '', unhex('%s')) ON DUPLICATE KEY UPDATE newMD5=unhex('%s'), knownFile=%d", $file, $file, $knownFile, $md5, $md5, $knownFile);
440
-
441
- $this->filesProcessedBloomFilter->add($file);
442
443
$this->totalFiles++;
444
- $this->totalData += filesize($realFile); //We already checked if file overflows int in the fileTooBig routine above
445
if($this->totalFiles % 100 === 0){
446
wordfence::status(2, 'info', "Analyzed " . $this->totalFiles . " files containing " . wfUtils::formatBytes($this->totalData) . " of data so far");
447
}
@@ -471,16 +569,7 @@ class wordfenceHash {
471
}
472
private function _shouldHashFile($fullPath) {
473
$file = substr($fullPath, $this->striplen);
474
-
475
- //Already hashed and processed, return false
476
- if ($this->filesProcessedBloomFilter->contains($file)) { //Might have hashed it, verify
477
- $rec = $this->db->querySingleRec("SELECT * FROM " . $this->db->prefix() . "wfFileMods WHERE filename = '%s'", $file);
478
- if ($rec !== null) {
479
- wordfence::status(4, 'info', "Already hashed: {$file}");
480
- return false;
481
- }
482
- }
483
-
484
//Core File, return true
485
if ((isset($this->knownFiles['core']) && isset($this->knownFiles['core'][$file])) ||
486
(isset($this->knownFiles['plugins']) && isset($this->knownFiles['plugins'][$file])) ||
@@ -489,7 +578,7 @@ class wordfenceHash {
489
}
490
491
//Excluded file, return false
492
- $excludePattern = wordfenceScanner::getExcludeFilePattern(wordfenceScanner::EXCLUSION_PATTERNS_USER & wordfenceScanner::EXCLUSION_PATTERNS_MALWARE);
493
if ($excludePattern && preg_match($excludePattern, $file)) {
494
return false;
495
}
25
private $only = false;
26
private $totalForks = 0;
27
private $alertedOnUnknownWordPressVersion = false;
28
+ private $foldersEntered = array();
29
private $foldersProcessed = array();
30
private $suspectedFiles = array();
31
+ private $indexed = false;
32
+ private $indexSize = 0;
33
+ private $currentIndex = 0;
34
35
/**
36
* @param string $striplen
64
if(wfConfig::get('scansEnabled_coreUnknown')){
65
$this->coreUnknownEnabled = true;
66
}
67
68
$this->db = new wfDB();
69
70
//Doing a delete for now. Later we can optimize this to only scan modified files.
71
//$this->db->queryWrite("update " . $this->db->prefix() . "wfFileMods set oldMD5 = newMD5");
72
$this->db->truncate($this->db->prefix() . "wfFileMods");
73
+ $this->db->truncate($this->db->prefix() . "wfKnownFileList");
74
$fetchCoreHashesStatus = wordfence::statusStart("Fetching core, theme and plugin file signatures from Wordfence");
75
try {
76
$this->knownFiles = $this->engine->getKnownFilesLoader()
118
if($this->coreUnknownEnabled){ $this->status['coreUnknown'] = wordfence::statusStart("Scanning for unknown files in wp-admin and wp-includes"); } else { wordfence::statusDisabled("Skipping unknown core file scan"); }
119
}
120
public function __sleep(){
121
+ return array('striplen', 'totalFiles', 'totalDirs', 'totalData', 'stoppedOnFile', 'coreEnabled', 'pluginsEnabled', 'themesEnabled', 'malwareEnabled', 'coreUnknownEnabled', 'knownFiles', 'malwareData', 'haveIssues', 'status', 'possibleMalware', 'path', 'only', 'totalForks', 'alertedOnUnknownWordPressVersion', 'foldersProcessed', 'suspectedFiles', 'indexed', 'indexSize', 'currentIndex', 'foldersEntered');
122
}
123
public function __wakeup(){
124
$this->db = new wfDB();
133
throw new Exception("Wordfence file scanner detected a possible infinite loop. Exiting on file: " . $this->stoppedOnFile);
134
}
135
$this->engine = $engine;
136
+ wordfence::status(4, 'info', "Indexing files for scanning");
137
+ if (!$this->indexed) {
138
+ $start = microtime(true);
139
+ $indexedFiles = array();
140
+
141
+ if (count($this->only) > 0) {
142
+ $files = $this->only;
143
}
144
+ else {
145
+ $files = scandir($this->path);
146
+ }
147
+
148
+ foreach ($files as $file) {
149
+ if ($file == '.' || $file == '..') { continue; }
150
+ $file = $this->path . $file;
151
+ $this->_dirIndex($file, $indexedFiles);
152
+ }
153
+ $this->_serviceIndexQueue($indexedFiles, true);
154
+ $this->indexed = true;
155
+ $end = microtime(true);
156
+ wordfence::status(4, 'info', "Index time: " . ($end - $start));
157
}
158
+
159
+ $this->_checkForTimeout('');
160
+
161
+ wordfence::status(4, 'info', "Beginning file hashing");
162
+ while ($file = $this->_nextFile()) {
163
+ $this->processFile($file);
164
+ $this->_checkForTimeout($file);
165
+ }
166
+
167
wordfence::status(2, 'info', "Analyzed " . $this->totalFiles . " files containing " . wfUtils::formatBytes($this->totalData) . " of data.");
168
if($this->coreEnabled){ wordfence::statusEnd($this->status['core'], $this->haveIssues['core']); }
169
if($this->themesEnabled){ wordfence::statusEnd($this->status['themes'], $this->haveIssues['themes']); }
202
}
203
if($this->malwareEnabled){ wordfence::statusEnd($this->status['malware'], $this->haveIssues['malware']); }
204
}
205
+ private function _dirIndex($path, &$indexedFiles) {
206
+ if (substr($path, -3, 3) == '/..' || substr($path, -2, 2) == '/.') {
207
return;
208
}
209
+ if (!is_readable($path)) { return; } //Applies to files and dirs
210
if (!$this->_shouldProcessPath($path)) { return; }
211
+ if (is_dir($path)) {
212
+ $realPath = realpath($path);
213
+ if (!$this->stoppedOnFile && isset($this->foldersEntered[$realPath])) { //Not resuming and already entered this path
214
+ return;
215
+ }
216
+
217
+ $this->foldersEntered[$realPath] = 1;
218
+
219
$this->totalDirs++;
220
+ if ($path[strlen($path) - 1] != '/') {
221
$path .= '/';
222
}
223
$cont = scandir($path);
224
+ for ($i = 0; $i < sizeof($cont); $i++) {
225
+ if ($cont[$i] == '.' || $cont[$i] == '..') { continue; }
226
$file = $path . $cont[$i];
227
+ if (is_file($file)) {
228
+ $relativeFile = substr($file, $this->striplen);
229
+ if ($this->stoppedOnFile && $relativeFile != $this->stoppedOnFile) {
230
+ continue;
231
+ }
232
+
233
+ if (preg_match('/\.suspected#x2F;i', $relativeFile)) { //Already iterating over all files in the search areas so generate this list here
234
+ wordfence::status(4, 'info', "Found .suspected file: {$relativeFile}");
235
+ $this->suspectedFiles[$relativeFile] = 1;
236
+ }
237
+
238
+ $this->_checkForTimeout($file, $indexedFiles);
239
+ if ($this->_shouldHashFile($file)) {
240
+ $indexedFiles[] = $relativeFile;
241
+ }
242
+ else {
243
+ wordfence::status(4, 'info', "Skipping unneeded hash: {$file}");
244
+ }
245
+ $this->_serviceIndexQueue($indexedFiles);
246
+ } else if (is_dir($file)) {
247
+ $this->_dirIndex($file, $indexedFiles);
248
}
249
}
250
251
+ $this->foldersProcessed[$realPath] = 1;
252
+ unset($this->foldersEntered[$realPath]);
253
+ }
254
+ else {
255
+ if (is_file($path)) {
256
+ $relativeFile = substr($path, $this->striplen);
257
+ if ($this->stoppedOnFile && $relativeFile != $this->stoppedOnFile) {
258
+ return;
259
+ }
260
+
261
+ if (preg_match('/\.suspected#x2F;i', $relativeFile)) { //Already iterating over all files in the search areas so generate this list here
262
+ wordfence::status(4, 'info', "Found .suspected file: {$relativeFile}");
263
+ $this->suspectedFiles[$relativeFile] = 1;
264
+ }
265
+
266
+ $this->_checkForTimeout($path, $indexedFiles);
267
+ if ($this->_shouldHashFile($path)) {
268
+ $indexedFiles[] = substr($path, $this->striplen);
269
+ }
270
+ else {
271
+ wordfence::status(4, 'info', "Skipping unneeded hash: {$path}");
272
+ }
273
+ $this->_serviceIndexQueue($indexedFiles);
274
}
275
}
276
}
277
+ private function _serviceIndexQueue(&$indexedFiles, $final = false) {
278
+ $payload = array();
279
+ if (count($indexedFiles) > 500) {
280
+ $payload = array_splice($indexedFiles, 0, 500);
281
+ }
282
+ else if ($final) {
283
+ $payload = $indexedFiles;
284
+ $indexedFiles = array();
285
+ }
286
+
287
+ if (count($payload) > 0) {
288
+ global $wpdb;
289
+ $query = substr("INSERT INTO {$wpdb->base_prefix}wfKnownFileList (path) VALUES " . str_repeat("('%s'), ", count($payload)), 0, -2);
290
+ $wpdb->query($wpdb->prepare($query, $payload));
291
+ $this->indexSize += count($payload);
292
+ wordfence::status(2, 'info', "{$this->indexSize} files indexed");
293
+ }
294
+ }
295
+ private function _nextFile($advanceCursor = true) {
296
+ static $files = array();
297
+ if (count($files) == 0) {
298
+ global $wpdb;
299
+ $files = $wpdb->get_col($wpdb->prepare("SELECT path FROM {$wpdb->base_prefix}wfKnownFileList WHERE id > %d ORDER BY id ASC LIMIT 500", $this->currentIndex));
300
+ }
301
+
302
+ $file = null;
303
+ if ($advanceCursor) {
304
+ $file = array_shift($files);
305
+ $this->currentIndex++;
306
+ }
307
+ else if (count($files) > 0) {
308
+ $file = $files[0];
309
+ }
310
+
311
+ if ($file === null) {
312
+ return null;
313
+ }
314
+ return ABSPATH . $file;
315
+ }
316
+ private function _checkForTimeout($path, $indexQueue = false) {
317
+ $this->engine->checkForKill();
318
+ $this->engine->checkForDurationLimit();
319
$file = substr($path, $this->striplen);
320
if ((!$this->stoppedOnFile) && microtime(true) - $this->startTime > $this->engine->maxExecTime) { //max X seconds but don't allow fork if we're looking for the file we stopped on. Search mode is VERY fast.
321
+ if ($indexQueue !== false) {
322
+ $this->_serviceIndexQueue($indexQueue, true);
323
+ $this->stoppedOnFile = $file;
324
+ wordfence::status(4, 'info', "Forking during indexing: " . $path);
325
+ }
326
+ else {
327
+ wordfence::status(4, 'info', "Calling fork() from wordfenceHash with maxExecTime: " . $this->engine->maxExecTime);
328
+ }
329
$this->engine->fork();
330
//exits
331
}
332
333
+ if ($this->stoppedOnFile && $file != $this->stoppedOnFile && $indexQueue !== false) {
334
return;
335
}
336
else if ($this->stoppedOnFile && $file == $this->stoppedOnFile) {
337
+ $this->stoppedOnFile = false; //Continue indexing
338
}
339
}
340
private function _shouldProcessPath($path) {
354
private function processFile($realFile){
355
$file = substr($realFile, $this->striplen);
356
357
if(wfUtils::fileTooBig($realFile)){
358
wordfence::status(4, 'info', "Skipping file larger than max size: $realFile");
359
return;
360
}
361
+
362
if (function_exists('memory_get_usage')) {
363
wordfence::status(4, 'info', "Scanning: $realFile (Mem:" . sprintf('%.1f', memory_get_usage(true) / (1024 * 1024)) . "M)");
364
} else {
537
// we could split this into files who's path we recognize and file's who's path we recognize AND who have a valid sig.
538
// But because we want to scan files who's sig we don't recognize, regardless of known path or not, we only need one "knownFile" field.
539
$this->db->queryWrite("insert into " . $this->db->prefix() . "wfFileMods (filename, filenameMD5, knownFile, oldMD5, newMD5) values ('%s', unhex(md5('%s')), %d, '', unhex('%s')) ON DUPLICATE KEY UPDATE newMD5=unhex('%s'), knownFile=%d", $file, $file, $knownFile, $md5, $md5, $knownFile);
540
541
$this->totalFiles++;
542
+ $this->totalData += @filesize($realFile); //We already checked if file overflows int in the fileTooBig routine above
543
if($this->totalFiles % 100 === 0){
544
wordfence::status(2, 'info', "Analyzed " . $this->totalFiles . " files containing " . wfUtils::formatBytes($this->totalData) . " of data so far");
545
}
569
}
570
private function _shouldHashFile($fullPath) {
571
$file = substr($fullPath, $this->striplen);
572
+
573
//Core File, return true
574
if ((isset($this->knownFiles['core']) && isset($this->knownFiles['core'][$file])) ||
575
(isset($this->knownFiles['plugins']) && isset($this->knownFiles['plugins'][$file])) ||
578
}
579
580
//Excluded file, return false
581
+ $excludePattern = wordfenceScanner::getExcludeFilePattern(wordfenceScanner::EXCLUSION_PATTERNS_USER | wordfenceScanner::EXCLUSION_PATTERNS_MALWARE);
582
if ($excludePattern && preg_match($excludePattern, $file)) {
583
return false;
584
}
lib/wordfenceScanner.php CHANGED
@@ -177,7 +177,7 @@ class wordfenceScanner {
177
}
178
$db = new wfDB();
179
$lastCount = 'whatever';
180
- $excludePattern = self::getExcludeFilePattern(self::EXCLUSION_PATTERNS_USER & self::EXCLUSION_PATTERNS_MALWARE);
181
while(true){
182
$thisCount = $db->querySingle("select count(*) from " . $db->prefix() . "wfFileMods where oldMD5 != newMD5 and knownFile=0");
183
if($thisCount == $lastCount){
@@ -195,6 +195,9 @@ class wordfenceScanner {
195
if($excludePattern && preg_match($excludePattern, $file)){
196
continue;
197
}
198
$fileSum = $rec1['newMD5'];
199
200
$fileExt = '';
@@ -244,7 +247,7 @@ class wordfenceScanner {
244
}
245
wfUtils::beginProcessingFile($file);
246
247
- $fsize = filesize($this->path . $file); //Checked if too big above
248
if($fsize > 1000000){
249
$fsize = sprintf('%.2f', ($fsize / 1000000)) . "M";
250
} else {
177
}
178
$db = new wfDB();
179
$lastCount = 'whatever';
180
+ $excludePattern = self::getExcludeFilePattern(self::EXCLUSION_PATTERNS_USER | self::EXCLUSION_PATTERNS_MALWARE);
181
while(true){
182
$thisCount = $db->querySingle("select count(*) from " . $db->prefix() . "wfFileMods where oldMD5 != newMD5 and knownFile=0");
183
if($thisCount == $lastCount){
195
if($excludePattern && preg_match($excludePattern, $file)){
196
continue;
197
}
198
+ if (!file_exists($this->path . $file)) {
199
+ continue;
200
+ }
201
$fileSum = $rec1['newMD5'];
202
203
$fileExt = '';
247
}
248
wfUtils::beginProcessingFile($file);
249
250
+ $fsize = @filesize($this->path . $file); //Checked if too big above
251
if($fsize > 1000000){
252
$fsize = sprintf('%.2f', ($fsize / 1000000)) . "M";
253
} else {
readme.txt CHANGED
@@ -3,7 +3,7 @@ Contributors: mmaunder
3
Tags: security, secure, security plugin, wordpress security, login security, firewall, malware, antivirus, web application firewall, block hackers, country blocking
4
Requires at least: 3.9
5
Tested up to: 4.6.1
6
- Stable tag: 6.2.3
7
8
Secure your website with the most comprehensive WordPress security plugin. Firewall, malware scan, blocking, live traffic, login security & more.
9
@@ -190,6 +190,20 @@ Secure your website with Wordfence.
190
191
== Changelog ==
192
193
= 6.2.3 =
194
* Improvement: Reworked blocking for IP ranges, country blocking, and direct IP blocking to minimize server impact when under attack.
195
* Improvement: Live traffic better indicates the action taken by country blocking when it redirects a visitor.
3
Tags: security, secure, security plugin, wordpress security, login security, firewall, malware, antivirus, web application firewall, block hackers, country blocking
4
Requires at least: 3.9
5
Tested up to: 4.6.1
6
+ Stable tag: 6.2.4
7
8
Secure your website with the most comprehensive WordPress security plugin. Firewall, malware scan, blocking, live traffic, login security & more.
9
190
191
== Changelog ==
192
193
+ = 6.2.4 =
194
+ * Improvement: Scan times for very large sites with huge numbers of files are greatly improved.
195
+ * Improvement: Added a configurable time limit for scans to help reduce overall server load and identify configuration problems.
196
+ * Improvement: Email-based logins are now covered by "Don't let WordPress reveal valid users in login errors".
197
+ * Improvement: Extended rate limiting support to the login page.
198
+ * Fix: Fixed a case where files in the site root with issues could have them added multiple times.
199
+ * Fix: Improved IP detection in the WAF when using an IP detection method that can have multiple values.
200
+ * Fix: Added a safety check for when the database fails to return its max_allowed_packet value.
201
+ * Fix: Added safety checks for when the configuration table migration has failed.
202
+ * Fix: Added a couple rare failed login error codes to brute force detection.
203
+ * Fix: Fixed a sequencing problem when adding detection for bot/human that led to it being called on every request.
204
+ * Fix: Suppressed errors if a file is removed between the start of a scan and later scan stages.
205
+ * Fix: Addressed a problem where the scan exclusions list was not checked correctly in some situations.
206
+
207
= 6.2.3 =
208
* Improvement: Reworked blocking for IP ranges, country blocking, and direct IP blocking to minimize server impact when under attack.
209
* Improvement: Live traffic better indicates the action taken by country blocking when it redirects a visitor.
waf/bootstrap.php CHANGED
@@ -31,15 +31,135 @@ class wfWAFWordPressRequest extends wfWAFRequest {
31
public function getIP() {
32
$howGet = wfWAF::getInstance()->getStorageEngine()->getConfig('howGetIPs');
33
if (is_string($howGet) && is_array($_SERVER) && array_key_exists($howGet, $_SERVER)) {
34
- $ips[] = $_SERVER[$howGet];
35
}
36
- $ips[] = $ip = (is_array($_SERVER) && array_key_exists('REMOTE_ADDR', $_SERVER)) ? $_SERVER['REMOTE_ADDR'] : '127.0.0.1';
37
- foreach ($ips as $ip) {
38
- if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false) {
39
- return $ip;
40
}
41
}
42
- return $ip;
43
}
44
}
45
31
public function getIP() {
32
$howGet = wfWAF::getInstance()->getStorageEngine()->getConfig('howGetIPs');
33
if (is_string($howGet) && is_array($_SERVER) && array_key_exists($howGet, $_SERVER)) {
34
+ $ips[] = array($_SERVER[$howGet], $howGet);
35
}
36
+ $ips[] = array((is_array($_SERVER) && array_key_exists('REMOTE_ADDR', $_SERVER)) ? $_SERVER['REMOTE_ADDR'] : '127.0.0.1', 'REMOTE_ADDR');
37
+ $cleanedIP = $this->_getCleanIPAndServerVar($ips);
38
+ if (is_array($cleanedIP)) {
39
+ list($ip, $variable) = $cleanedIP;
40
+ return $ip;
41
+ }
42
+ return $cleanedIP;
43
+ }
44
+
45
+ /**
46
+ * Expects an array of items. The items are either IPs or IPs separated by comma, space or tab. Or an array of IP's.
47
+ * We then examine all IP's looking for a public IP and storing private IP's in an array. If we find no public IPs we return the first private addr we found.
48
+ *
49
+ * @param array $arr
50
+ * @return bool|mixed
51
+ */
52
+ private function _getCleanIPAndServerVar($arr) {
53
+ $privates = array(); //Store private addrs until end as last resort.
54
+ foreach ($arr as $entry) {
55
+ list($item, $var) = $entry;
56
+ if (is_array($item)) {
57
+ foreach ($item as $j) {
58
+ // try verifying the IP is valid before stripping the port off
59
+ if (!$this->_isValidIP($j)) {
60
+ $j = preg_replace('/:\d+#x2F;', '', $j); //Strip off port
61
+ }
62
+ if ($this->_isValidIP($j)) {
63
+ if ($this->_isIPv6MappedIPv4($j)) {
64
+ $j = wfWAFUtils::inet_ntop(wfWAFUtils::inet_pton($j));
65
+ }
66
+
67
+ if ($this->_isPrivateIP($j)) {
68
+ $privates[] = array($j, $var);
69
+ }
70
+ else {
71
+ return array($j, $var);
72
+ }
73
+ }
74
+ }
75
+ continue; //This was an array so we can skip to the next item
76
}
77
+ $skipToNext = false;
78
+ foreach (array(',', ' ', "\t") as $char) {
79
+ if (strpos($item, $char) !== false) {
80
+ $sp = explode($char, $item);
81
+ foreach ($sp as $j) {
82
+ $j = trim($j);
83
+ if (!$this->_isValidIP($j)) {
84
+ $j = preg_replace('/:\d+#x2F;', '', $j); //Strip off port
85
+ }
86
+ if ($this->_isValidIP($j)) {
87
+ if ($this->_isIPv6MappedIPv4($j)) {
88
+ $j = wfWAFUtils::inet_ntop(wfWAFUtils::inet_pton($j));
89
+ }
90
+
91
+ if ($this->_isPrivateIP($j)) {
92
+ $privates[] = array($j, $var);
93
+ }
94
+ else {
95
+ return array($j, $var);
96
+ }
97
+ }
98
+ }
99
+ $skipToNext = true;
100
+ break;
101
+ }
102
+ }
103
+ if ($skipToNext){ continue; } //Skip to next item because this one had a comma, space or tab so was delimited and we didn't find anything.
104
+
105
+ if (!$this->_isValidIP($item)) {
106
+ $item = preg_replace('/:\d+#x2F;', '', $item); //Strip off port
107
+ }
108
+ if ($this->_isValidIP($item)) {
109
+ if ($this->_isIPv6MappedIPv4($item)) {
110
+ $item = wfWAFUtils::inet_ntop(wfWAFUtils::inet_pton($item));
111
+ }
112
+
113
+ if ($this->_isPrivateIP($item)) {
114
+ $privates[] = array($item, $var);
115
+ }
116
+ else {
117
+ return array($item, $var);
118
+ }
119
+ }
120
+ }
121
+ if (sizeof($privates) > 0) {
122
+ return $privates[0]; //Return the first private we found so that we respect the order the IP's were passed to this function.
123
}
124
+ return false;
125
+ }
126
+
127
+ /**
128
+ * @param string $ip
129
+ * @return bool
130
+ */
131
+ private function _isValidIP($ip) {
132
+ return filter_var($ip, FILTER_VALIDATE_IP) !== false;
133
+ }
134
+
135
+ /**
136
+ * @param string $ip
137
+ * @return bool
138
+ */
139
+ private function _isIPv6MappedIPv4($ip) {
140
+ return preg_match('/^(?:\:(?:\:0{1,4}){0,4}\:|(?:0{1,4}\:){5})ffff\:\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}#x2F;i', $ip) > 0;
141
+ }
142
+
143
+ /**
144
+ * @param string $addr Should be in dot or colon notation (127.0.0.1 or ::1)
145
+ * @return bool
146
+ */
147
+ private function _isPrivateIP($ip) {
148
+ // Run this through the preset list for IPv4 addresses.
149
+ if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false) {
150
+ $wordfenceLib = realpath(dirname(__FILE__) . '/../lib');
151
+ include($wordfenceLib . '/wfIPWhitelist.php'); // defines $wfIPWhitelist
152
+ $private = $wfIPWhitelist['private'];
153
+
154
+ foreach ($private as $a) {
155
+ if (wfWAFUtils::subnetContainsIP($a, $ip)) {
156
+ return true;
157
+ }
158
+ }
159
+ }
160
+
161
+ return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6) !== false
162
+ && filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false;
163
}
164
}
165
wordfence.php CHANGED
@@ -4,14 +4,14 @@ Plugin Name: Wordfence Security
4
Plugin URI: http://www.wordfence.com/
5
Description: Wordfence Security - Anti-virus, Firewall and Malware Scan
6
Author: Wordfence
7
- Version: 6.2.3
8
Author URI: http://www.wordfence.com/
9
Network: true
10
*/
11
if(defined('WP_INSTALLING') && WP_INSTALLING){
12
return;
13
}
14
- define('WORDFENCE_VERSION', '6.2.3');
15
define('WORDFENCE_BASENAME', function_exists('plugin_basename') ? plugin_basename(__FILE__) :
16
basename(dirname(__FILE__)) . '/' . basename(__FILE__));
17
4
Plugin URI: http://www.wordfence.com/
5
Description: Wordfence Security - Anti-virus, Firewall and Malware Scan
6
Author: Wordfence
7
+ Version: 6.2.4
8
Author URI: http://www.wordfence.com/
9
Network: true
10
*/
11
if(defined('WP_INSTALLING') && WP_INSTALLING){
12
return;
13
}
14
+ define('WORDFENCE_VERSION', '6.2.4');
15
define('WORDFENCE_BASENAME', function_exists('plugin_basename') ? plugin_basename(__FILE__) :
16
basename(dirname(__FILE__)) . '/' . basename(__FILE__));
17