Wordfence Security – Firewall & Malware Scan - Version 2.1.0

Version Description

  • Fixed scans hanging on Dreamhost and other hosts.
  • Made Wordfence more memory efficient.
  • Wordfence scans are now broken into steps so we can scan a huge number of files, posts and comments.
  • Alert emails now include IP address, hostname lookup and geographic location (city if available).
  • Improved scan locking. No longer time based but uses flock() if on unix or time on Windows.
  • Suppressed warnings that WF was generating.
  • Improve handling of non-standard wp-content directories.
  • Fix restored files were still showing as changed if they contained international characters.
  • Improve permission denied message if attempting to repair a file.
  • Fixed problem that caused scans to not start because some hosts take too long to look up their own name.
  • Fixed issue with Wordfence menu that caused it to not appear or conflict with other menus under certain conditions.
  • Upgraded to API version 1.6
  • Improved geo lookup code for IP's.
  • Fixed debug mode output in live status box - coloring was wrong.
  • Added ajax status message to WF admin pages.
  • Fixed colorbox popup so that it doesn't jump around on refresh.
Download this release

Release Info

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

Code changes from version 2.0.7 to 2.1.0

css/main.css CHANGED
@@ -268,4 +268,20 @@ input.wfStartScanButton { width: 160px; text-align: left; padding-left: 20px; }
268
.wferror {
269
color: #F00;
270
}
271
-
268
.wferror {
269
color: #F00;
270
}
271
+ #wordfenceWorking {
272
+ padding: 2px 8px 2px 24px;
273
+ z-index: 100000;
274
+ position: fixed;
275
+ right: 2px;
276
+ bottom: 2px;
277
+ border: 1px solid #000;
278
+ background-color: #F00;
279
+ color: #FFF;
280
+ font-size: 12px;
281
+ font-weight: bold;
282
+ font-family: Arial;
283
+ text-align: center;
284
+ background-image: url('../images/icons/ajaxRed16.gif');
285
+ background-position: 2px 2px;
286
+ background-repeat: no-repeat;
287
+ }
images/icons/ajaxRed16.gif ADDED
Binary file
js/admin.js CHANGED
@@ -63,8 +63,16 @@ window['wordfenceAdmin'] = {
63
}
64
jQuery(document).bind('cbox_closed', function(){ self.colorboxIsOpen = false; self.colorboxServiceQueue(); });
65
}
66
67
},
68
startActivityLogUpdates: function(){
69
var self = this;
70
setInterval(function(){
@@ -79,7 +87,7 @@ window['wordfenceAdmin'] = {
79
var self = this;
80
this.ajax('wordfence_activityLogUpdate', {
81
lastctime: this.lastALogCtime
82
- }, function(res){ self.doneUpdateActivityLog(res); }, function(){ self.activityLogUpdatePending = false; });
83
84
},
85
doneUpdateActivityLog: function(res){
@@ -216,7 +224,7 @@ window['wordfenceAdmin'] = {
216
this.ajax('wordfence_ticker', {
217
alsoGet: alsoGet,
218
otherParams: otherParams
219
- }, function(res){ self.handleTickerReturn(res); }, function(){ self.tickerUpdatePending = false; });
220
},
221
handleTickerReturn: function(res){
222
this.tickerUpdatePending = false;
@@ -314,8 +322,7 @@ window['wordfenceAdmin'] = {
314
}
315
});
316
}
317
- }
318
- );
319
},
320
activateWF: function(key){
321
jQuery('.wfAjax24').show();
@@ -441,7 +448,7 @@ window['wordfenceAdmin'] = {
441
}
442
return true;
443
},
444
- ajax: function(action, data, cb, cbErr){
445
if(typeof(data) == 'string'){
446
if(data.length > 0){
447
data += '&';
@@ -455,12 +462,16 @@ window['wordfenceAdmin'] = {
455
cbErr = function(){};
456
}
457
var self = this;
458
jQuery.ajax({
459
type: 'POST',
460
url: WordfenceAdminVars.ajaxURL,
461
dataType: "json",
462
data: data,
463
success: function(json){
464
if(json && json.nonce){
465
self.nonce = json.nonce;
466
}
@@ -469,7 +480,7 @@ window['wordfenceAdmin'] = {
469
}
470
cb(json);
471
},
472
- error: cbErr
473
});
474
},
475
colorbox: function(width, heading, body){
@@ -495,11 +506,12 @@ window['wordfenceAdmin'] = {
495
}, function(res){ self.doneDeleteFile(res); });
496
},
497
doneDeleteFile: function(res){
498
if(res.ok){
499
- var self = this;
500
this.loadIssues(function(){ self.colorbox('400px', "Success deleting file", "The file " + res.file + " containing " + res.filesize + " bytes was successfully deleted."); });
501
- } else if(res.errorMsg){
502
- this.loadIssues();
503
}
504
},
505
restoreFile: function(issueID){
@@ -509,26 +521,24 @@ window['wordfenceAdmin'] = {
509
}, function(res){ self.doneRestoreFile(res); });
510
},
511
doneRestoreFile: function(res){
512
- this.loadIssues();
513
if(res.ok){
514
- this.colorbox("400px", "File restored OK", "The file " + res.file + " was restored succesfully.");
515
}
516
},
517
deleteIssue: function(id){
518
var self = this;
519
this.ajax('wordfence_deleteIssue', { id: id }, function(res){
520
self.loadIssues();
521
- if(res.errMsg){
522
- self.colorbox('400px', "An error occured", res.errMsg);
523
- }
524
});
525
},
526
updateIssueStatus: function(id, st){
527
var self = this;
528
this.ajax('wordfence_updateIssueStatus', { id: id, 'status': st }, function(res){
529
- self.loadIssues();
530
- if(res.errMsg){
531
- self.colorbox('400px', "An error occured", res.errMsg);
532
}
533
});
534
},
63
}
64
jQuery(document).bind('cbox_closed', function(){ self.colorboxIsOpen = false; self.colorboxServiceQueue(); });
65
}
66
+ this.showLoading();
67
68
},
69
+ showLoading: function(){
70
+ this.removeLoading();
71
+ jQuery('<div id="wordfenceWorking">Wordfence is working...</div>').appendTo('body');
72
+ },
73
+ removeLoading: function(){
74
+ jQuery('#wordfenceWorking').remove();
75
+ },
76
startActivityLogUpdates: function(){
77
var self = this;
78
setInterval(function(){
87
var self = this;
88
this.ajax('wordfence_activityLogUpdate', {
89
lastctime: this.lastALogCtime
90
+ }, function(res){ self.doneUpdateActivityLog(res); }, function(){ self.activityLogUpdatePending = false; }, true);
91
92
},
93
doneUpdateActivityLog: function(res){
224
this.ajax('wordfence_ticker', {
225
alsoGet: alsoGet,
226
otherParams: otherParams
227
+ }, function(res){ self.handleTickerReturn(res); }, function(){ self.tickerUpdatePending = false; }, true);
228
},
229
handleTickerReturn: function(res){
230
this.tickerUpdatePending = false;
322
}
323
});
324
}
325
+ }, false, false);
326
},
327
activateWF: function(key){
328
jQuery('.wfAjax24').show();
448
}
449
return true;
450
},
451
+ ajax: function(action, data, cb, cbErr, noLoading){
452
if(typeof(data) == 'string'){
453
if(data.length > 0){
454
data += '&';
462
cbErr = function(){};
463
}
464
var self = this;
465
+ if(! noLoading){
466
+ this.showLoading();
467
+ }
468
jQuery.ajax({
469
type: 'POST',
470
url: WordfenceAdminVars.ajaxURL,
471
dataType: "json",
472
data: data,
473
success: function(json){
474
+ self.removeLoading();
475
if(json && json.nonce){
476
self.nonce = json.nonce;
477
}
480
}
481
cb(json);
482
},
483
+ error: function(){ self.removeLoading(); cbErr(); }
484
});
485
},
486
colorbox: function(width, heading, body){
506
}, function(res){ self.doneDeleteFile(res); });
507
},
508
doneDeleteFile: function(res){
509
+ var cb = false;
510
+ var self = this;
511
if(res.ok){
512
this.loadIssues(function(){ self.colorbox('400px', "Success deleting file", "The file " + res.file + " containing " + res.filesize + " bytes was successfully deleted."); });
513
+ } else if(res.cerrorMsg){
514
+ this.loadIssues(function(){ self.colorbox('400px', 'An error occured', res.cerrorMsg); });
515
}
516
},
517
restoreFile: function(issueID){
521
}, function(res){ self.doneRestoreFile(res); });
522
},
523
doneRestoreFile: function(res){
524
+ var self = this;
525
if(res.ok){
526
+ this.loadIssues(function(){ self.colorbox("400px", "File restored OK", "The file " + res.file + " was restored succesfully."); });
527
+ } else if(res.cerrorMsg){
528
+ this.loadIssues(function(){ self.colorbox('400px', 'An error occured', res.cerrorMsg); });
529
}
530
},
531
deleteIssue: function(id){
532
var self = this;
533
this.ajax('wordfence_deleteIssue', { id: id }, function(res){
534
self.loadIssues();
535
});
536
},
537
updateIssueStatus: function(id, st){
538
var self = this;
539
this.ajax('wordfence_updateIssueStatus', { id: id, 'status': st }, function(res){
540
+ if(res.ok){
541
+ self.loadIssues();
542
}
543
});
544
},
lib/Diff.php CHANGED
@@ -173,4 +173,4 @@ class Diff
173
$this->groupedCodes = $sequenceMatcher->getGroupedOpcodes();
174
return $this->groupedCodes;
175
}
176
- }
173
$this->groupedCodes = $sequenceMatcher->getGroupedOpcodes();
174
return $this->groupedCodes;
175
}
176
+ }
lib/Diff/Renderer/Html/Array.php CHANGED
@@ -219,6 +219,6 @@ class Diff_Renderer_Html_Array extends Diff_Renderer_Abstract
219
*/
220
private function htmlSafe($string)
221
{
222
- return htmlspecialchars($string, ENT_NOQUOTES, 'UTF-8');
223
}
224
- }
219
*/
220
private function htmlSafe($string)
221
{
222
+ return @htmlspecialchars($string, ENT_NOQUOTES, 'UTF-8');
223
}
224
+ }
lib/Diff/SequenceMatcher.php CHANGED
@@ -739,4 +739,4 @@ class Diff_SequenceMatcher
739
return 1;
740
}
741
}
742
- }
739
return 1;
740
}
741
}
742
+ }
lib/diffResult.php CHANGED
@@ -30,7 +30,14 @@
30
?>
31
</table>
32
33
- <?php echo $diffResult; ?>
34
35
36
<div class="diffFooter">&copy;&nbsp;2011 Wordfence &mdash; Visit <a href="http://wordfence.com/">Wordfence.com</a> for help, security updates and more.</a>
30
?>
31
</table>
32
33
+ <?php
34
+ if($diffResult){
35
+ echo $diffResult;
36
+ } else {
37
+ echo "<br />There are no differences between the original file and the file in the repository.";
38
+ }
39
+
40
+ ?>
41
42
43
<div class="diffFooter">&copy;&nbsp;2011 Wordfence &mdash; Visit <a href="http://wordfence.com/">Wordfence.com</a> for help, security updates and more.</a>
lib/email_genericAlert.php CHANGED
@@ -2,7 +2,7 @@ This alert was generated by Wordfence on "<?php echo $blogName; ?>" at <?php ech
2
3
4
<?php echo $alertMsg; ?>
5
-
6
7
--
8
To change your alert options for Wordfence, visit:
2
3
4
<?php echo $alertMsg; ?>
5
+ <?php if($IPMsg){ echo "\n$IPMsg\n"; } ?>
6
7
--
8
To change your alert options for Wordfence, visit:
lib/menu_scan.php CHANGED
@@ -43,7 +43,11 @@
43
foreach($events as $e){
44
if(strpos($e['msg'], 'SUM_') !== 0){
45
if( $debugOn || $e['level'] < 4){
46
- echo '<div class="wfActivityLine wf' . $e['type'] . '">[' . date('M d H:i:s', $e['ctime']) . ']&nbsp;' . $e['msg'] . '</div>';
47
}
48
}
49
$newestItem = $e['ctime'];
43
foreach($events as $e){
44
if(strpos($e['msg'], 'SUM_') !== 0){
45
if( $debugOn || $e['level'] < 4){
46
+ $typeClass = '';
47
+ if($debugOn){
48
+ $typeClass = ' wf' . $e['type'];
49
+ }
50
+ echo '<div class="wfActivityLine' . $typeClass . '">[' . date('M d H:i:s', $e['ctime']) . ']&nbsp;' . $e['msg'] . '</div>';
51
}
52
}
53
$newestItem = $e['ctime'];
lib/wfConfig.php CHANGED
@@ -368,7 +368,7 @@ class wfConfig {
368
}
369
public static function set($key, $val){
370
if(is_array($val)){
371
- $trace=debug_backtrace(); $caller=array_shift($trace); error_log("wfConfig::set() got array as second param. Please use ser_ser(). " . $caller['file'] . " line " . $caller['line']);
372
}
373
374
self::getDB()->query("insert into " . self::table() . " (name, val) values ('%s', '%s') ON DUPLICATE KEY UPDATE val='%s'", $key, $val, $val);
368
}
369
public static function set($key, $val){
370
if(is_array($val)){
371
+ $trace=debug_backtrace(); $caller=array_shift($trace); error_log("wfConfig::set() got array as second param. Please use set_ser(). " . $caller['file'] . " line " . $caller['line']);
372
}
373
374
self::getDB()->query("insert into " . self::table() . " (name, val) values ('%s', '%s') ON DUPLICATE KEY UPDATE val='%s'", $key, $val, $val);
lib/wfCrawl.php CHANGED
@@ -22,7 +22,7 @@ class wfCrawl {
22
}
23
}
24
$wfLog = new wfLog(wfConfig::get('apiKey'), wfUtils::getWPVersion());
25
- $host = $wfLog->reverseLookup($IP);
26
if(! $host){
27
$db->query("insert into $table (IP, patternSig, status, lastUpdate, PTR) values (%s, UNHEX(MD5('%s')), '%s', unix_timestamp(), '%s') ON DUPLICATE KEY UPDATE status='%s', lastUpdate=unix_timestamp(), PTR='%s'", $IPn, $hostPattern, 'noPTR', '', 'noPTR', '');
28
return false;
22
}
23
}
24
$wfLog = new wfLog(wfConfig::get('apiKey'), wfUtils::getWPVersion());
25
+ $host = wfUtils::reverseLookup($IP);
26
if(! $host){
27
$db->query("insert into $table (IP, patternSig, status, lastUpdate, PTR) values (%s, UNHEX(MD5('%s')), '%s', unix_timestamp(), '%s') ON DUPLICATE KEY UPDATE status='%s', lastUpdate=unix_timestamp(), PTR='%s'", $IPn, $hostPattern, 'noPTR', '', 'noPTR', '');
28
return false;
lib/wfDB.php CHANGED
@@ -37,6 +37,7 @@ class wfDB {
37
}
38
mysql_select_db($this->dbname, $dbh);
39
$this->dbh = $dbh;
40
} else {
41
$handleKey = md5($dbhost . $dbuser . $dbpassword . $dbname);
42
if(isset(self::$dbhCache[$handleKey])){
@@ -51,6 +52,7 @@ class wfDB {
51
mysql_select_db($this->dbname, $dbh);
52
self::$dbhCache[$handleKey] = $dbh;
53
$this->dbh = self::$dbhCache[$handleKey];
54
}
55
}
56
}
37
}
38
mysql_select_db($this->dbname, $dbh);
39
$this->dbh = $dbh;
40
+ $this->query("SET NAMES 'utf8'");
41
} else {
42
$handleKey = md5($dbhost . $dbuser . $dbpassword . $dbname);
43
if(isset(self::$dbhCache[$handleKey])){
52
mysql_select_db($this->dbname, $dbh);
53
self::$dbhCache[$handleKey] = $dbh;
54
$this->dbh = self::$dbhCache[$handleKey];
55
+ $this->query("SET NAMES 'utf8'");
56
}
57
}
58
}
lib/wfIssues.php CHANGED
@@ -1,18 +1,25 @@
1
<?php
2
require_once('wfUtils.php');
3
class wfIssues {
4
private $updateCalled = false;
5
- public $lastError = '';
6
private $issuesTable = '';
7
private $newIssues = array();
8
public $totalIssues = 0;
9
public $totalCriticalIssues = 0;
10
public $totalWarningIssues = 0;
11
- private $db = false;
12
public function __construct(){
13
global $wpdb;
14
$this->issuesTable = $wpdb->base_prefix . 'wfIssues';
15
}
16
public function addIssue($type, $severity,
17
18
$ignoreP, /* some piece of data used for md5 for permanent ignores */
@@ -30,12 +37,12 @@ class wfIssues {
30
if($rec['status'] == 'ignoreP' && $rec['ignoreP'] == $ignoreP){ return false; }
31
}
32
33
- $this->totalIssues++;
34
if($severity == 1){
35
$this->totalCriticalIssues++;
36
} else if($severity == 2){
37
$this->totalWarningIssues++;
38
}
39
$this->newIssues[] = array(
40
'type' => $type,
41
'severity' => $severity,
@@ -192,7 +199,7 @@ class wfIssues {
192
}
193
$arr = wfConfig::get_ser('wf_summaryItems', array());
194
//$arr['scanTimeAgo'] = wfUtils::makeTimeAgo(sprintf('%.0f', time() - $arr['scanTime']));
195
- $arr['scanRunning'] = wfConfig::get('wf_scanRunning') ? '1' : '0';
196
$arr['scheduledScansEnabled'] = wfConfig::get('scheduledScansEnabled');
197
$secsToGo = wp_next_scheduled('wordfence_scheduled_scan') - time();
198
if($secsToGo < 1){
1
<?php
2
require_once('wfUtils.php');
3
class wfIssues {
4
+ private $db = false;
5
+
6
+ //Properties that are serialized on sleep:
7
private $updateCalled = false;
8
private $issuesTable = '';
9
private $newIssues = array();
10
public $totalIssues = 0;
11
public $totalCriticalIssues = 0;
12
public $totalWarningIssues = 0;
13
+ public function __sleep(){ //Same order here as vars above
14
+ return array('updateCalled', 'issuesTable', 'newIssues', 'totalIssues', 'totalCriticalIssues', 'totalWarningIssues');
15
+ }
16
public function __construct(){
17
global $wpdb;
18
$this->issuesTable = $wpdb->base_prefix . 'wfIssues';
19
}
20
+ public function __wakeup(){
21
+ $this->db = new wfDB();
22
+ }
23
public function addIssue($type, $severity,
24
25
$ignoreP, /* some piece of data used for md5 for permanent ignores */
37
if($rec['status'] == 'ignoreP' && $rec['ignoreP'] == $ignoreP){ return false; }
38
}
39
40
if($severity == 1){
41
$this->totalCriticalIssues++;
42
} else if($severity == 2){
43
$this->totalWarningIssues++;
44
}
45
+ $this->totalIssues++;
46
$this->newIssues[] = array(
47
'type' => $type,
48
'severity' => $severity,
199
}
200
$arr = wfConfig::get_ser('wf_summaryItems', array());
201
//$arr['scanTimeAgo'] = wfUtils::makeTimeAgo(sprintf('%.0f', time() - $arr['scanTime']));
202
+ $arr['scanRunning'] = wfUtils::isScanRunning() ? '1' : '0';
203
$arr['scheduledScansEnabled'] = wfConfig::get('scheduledScansEnabled');
204
$secsToGo = wp_next_scheduled('wordfence_scheduled_scan') - time();
205
if($secsToGo < 1){
lib/wfLog.php CHANGED
@@ -4,7 +4,6 @@ require_once('wfUtils.php');
4
require_once('wfBrowscap.php');
5
class wfLog {
6
private $hitsTable = '';
7
- private $locsTable = '';
8
private $apiKey = '';
9
private $wp_version = '';
10
private $db = false;
@@ -16,13 +15,11 @@ class wfLog {
16
global $wpdb;
17
$this->hitsTable = $wpdb->base_prefix . 'wfHits';
18
$this->loginsTable = $wpdb->base_prefix . 'wfLogins';
19
- $this->locsTable = $wpdb->base_prefix . 'wfLocs';
20
$this->blocksTable = $wpdb->base_prefix . 'wfBlocks';
21
$this->lockOutTable = $wpdb->base_prefix . 'wfLockedOut';
22
$this->leechTable = $wpdb->base_prefix . 'wfLeechers';
23
$this->badLeechersTable = $wpdb->base_prefix . 'wfBadLeechers';
24
$this->scanTable = $wpdb->base_prefix . 'wfScanners';
25
- $this->reverseTable = $wpdb->base_prefix . 'wfReverseCache';
26
$this->throttleTable = $wpdb->base_prefix . 'wfThrottleLog';
27
$this->statusTable = $wpdb->base_prefix . 'wfStatus';
28
}
@@ -419,61 +416,8 @@ class wfLog {
419
$IPs[] = $res['IP'];
420
}
421
}
422
- $IPs = array_unique($IPs);
423
- $IPLocs = array();
424
- $toResolve = array();
425
- foreach($IPs as $IP){
426
- $r1 = $this->getDB()->query("select IP, ctime, failed, city, region, countryName, countryCode, lat, lon, unix_timestamp() - ctime as age from " . $this->locsTable . " where IP=%s", $IP);
427
- if($r1){
428
- if($row = mysql_fetch_assoc($r1)){
429
- if($row['age'] > WORDFENCE_MAX_IPLOC_AGE){
430
- $this->getDB()->query("delete from " . $this->locsTable . " where IP=%s", $row['IP']);
431
- } else {
432
- if($row['failed'] == 1){
433
- $IPLocs[$IP] = false;
434
- } else {
435
- $IPLocs[$IP] = $row;
436
- }
437
- }
438
- }
439
- }
440
- if(! isset($IPLocs[$IP])){
441
- $toResolve[] = $IP;
442
- }
443
- }
444
- if(sizeof($toResolve) > 0){
445
- $api = new wfAPI($this->apiKey, $this->wp_version);
446
- $freshIPs = $api->call('resolve_ips', array(), array(
447
- 'ips' => implode(',', $toResolve)
448
- ));
449
- if(is_array($freshIPs)){
450
- foreach($freshIPs as $IP => $value){
451
- if($value == 'failed'){
452
- $this->getDB()->query("insert IGNORE into " . $this->locsTable . " (IP, ctime, failed) values (%s, unix_timestamp(), 1)", $IP);
453
- $IPLocs[$IP] = false;
454
- } else {
455
- $this->getDB()->query("insert IGNORE into " . $this->locsTable . " (IP, ctime, failed, city, region, countryName, countryCode, lat, lon) values (%s, unix_timestamp(), 0, '%s', '%s', '%s', '%s', %s, %s)",
456
- $IP,
457
- $value[3], //city
458
- $value[2], //region
459
- $value[1], //countryName
460
- $value[0],//countryCode
461
- $value[4],//lat
462
- $value[5]//lon
463
- );
464
- $IPLocs[$IP] = array(
465
- 'IP' => $IP,
466
- 'city' => $value[3],
467
- 'region' => $value[2],
468
- 'countryName' => $value[1],
469
- 'countryCode' => $value[0],
470
- 'lat' => $value[4],
471
- 'lon' => $value[5]
472
- );
473
- }
474
- }
475
- }
476
- }
477
foreach($results as &$res){
478
if(isset($IPLocs[$res['IP']])){
479
$res['loc'] = $IPLocs[$res['IP']];
@@ -540,7 +484,7 @@ class wfLog {
540
if($action == 'block'){
541
$IP = wfUtils::getIP();
542
if(wfConfig::get('alertOn_block')){
543
- wordfence::alert("Blocking IP $IP", "Wordfence has blocked IP address $IP.\n The reason is: \"$reason\".\n When we did a reverse lookup on this address it resolved to:\n \"" . $this->reverseLookup($IP) . "\".");
544
}
545
wordfence::status(2, 'info', "Blocking IP $IP. $reason");
546
$this->blockIP($IP, $reason);
@@ -595,25 +539,6 @@ class wfLog {
595
}
596
return self::$gbSafeCache[$cacheKey]; //return cached value
597
}
598
- public function reverseLookup($IP){
599
- $IPn = wfUtils::inet_aton($IP);
600
- $host = $this->getDB()->querySingle("select host from " . $this->reverseTable . " where IP=%s and unix_timestamp() - lastUpdate < %d", $IPn, WORDFENCE_REVERSE_LOOKUP_CACHE_TIME);
601
- if(! $host){
602
- $ptr = implode(".", array_reverse(explode(".",$IP))) . ".in-addr.arpa";
603
- $host = dns_get_record($ptr, DNS_PTR);
604
- if($host == null){
605
- $host = 'NONE';
606
- } else {
607
- $host = $host[0]['target'];
608
- }
609
- $this->getDB()->query("insert into " . $this->reverseTable . " (IP, host, lastUpdate) values (%s, '%s', unix_timestamp()) ON DUPLICATE KEY UPDATE host='%s', lastUpdate=unix_timestamp()", $IPn, $host, $host);
610
- }
611
- if($host == 'NONE'){
612
- return '';
613
- } else {
614
- return $host;
615
- }
616
- }
617
public function addStatus($level, $type, $msg){
618
//$msg = '[' . sprintf('%.2f', memory_get_usage(true) / (1024 * 1024)) . '] ' . $msg;
619
$this->getDB()->query("insert into " . $this->statusTable . " (ctime, level, type, msg) values (%s, %d, '%s', '%s')", sprintf('%.6f', microtime(true)), $level, $type, $msg);
@@ -636,7 +561,7 @@ class wfLog {
636
return $results;
637
}
638
public function getSummaryEvents(){
639
- $res = $this->getDB()->query("select ctime, level, type, msg from " . $this->statusTable . " where level = 10 order by ctime desc limit 100", $lastCtime);
640
$results = array();
641
$lastTime = false;
642
while($rec = mysql_fetch_assoc($res)){
4
require_once('wfBrowscap.php');
5
class wfLog {
6
private $hitsTable = '';
7
private $apiKey = '';
8
private $wp_version = '';
9
private $db = false;
15
global $wpdb;
16
$this->hitsTable = $wpdb->base_prefix . 'wfHits';
17
$this->loginsTable = $wpdb->base_prefix . 'wfLogins';
18
$this->blocksTable = $wpdb->base_prefix . 'wfBlocks';
19
$this->lockOutTable = $wpdb->base_prefix . 'wfLockedOut';
20
$this->leechTable = $wpdb->base_prefix . 'wfLeechers';
21
$this->badLeechersTable = $wpdb->base_prefix . 'wfBadLeechers';
22
$this->scanTable = $wpdb->base_prefix . 'wfScanners';
23
$this->throttleTable = $wpdb->base_prefix . 'wfThrottleLog';
24
$this->statusTable = $wpdb->base_prefix . 'wfStatus';
25
}
416
$IPs[] = $res['IP'];
417
}
418
}
419
+ $IPLocs = wfUtils::getIPsGeo($IPs); //Creates an array with IP as key and data as value
420
+
421
foreach($results as &$res){
422
if(isset($IPLocs[$res['IP']])){
423
$res['loc'] = $IPLocs[$res['IP']];
484
if($action == 'block'){
485
$IP = wfUtils::getIP();
486
if(wfConfig::get('alertOn_block')){
487
+ wordfence::alert("Blocking IP $IP", "Wordfence has blocked IP address $IP.\nThe reason is: \"$reason\".", $IP);
488
}
489
wordfence::status(2, 'info', "Blocking IP $IP. $reason");
490
$this->blockIP($IP, $reason);
539
}
540
return self::$gbSafeCache[$cacheKey]; //return cached value
541
}
542
public function addStatus($level, $type, $msg){
543
//$msg = '[' . sprintf('%.2f', memory_get_usage(true) / (1024 * 1024)) . '] ' . $msg;
544
$this->getDB()->query("insert into " . $this->statusTable . " (ctime, level, type, msg) values (%s, %d, '%s', '%s')", sprintf('%.6f', microtime(true)), $level, $type, $msg);
561
return $results;
562
}
563
public function getSummaryEvents(){
564
+ $res = $this->getDB()->query("select ctime, level, type, msg from " . $this->statusTable . " where level = 10 order by ctime desc limit 100");
565
$results = array();
566
$lastTime = false;
567
while($rec = mysql_fetch_assoc($res)){
lib/wfScanEngine.php CHANGED
@@ -7,27 +7,67 @@ require_once('wfIssues.php');
7
require_once('wfDB.php');
8
require_once('wfUtils.php');
9
class wfScanEngine {
10
- private $i = false;
11
private $api = false;
12
- private $dbh = false;
13
private $wp_version = false;
14
private $apiKey = false;
15
private $errorStopped = false;
16
- private $dictWords = array();
17
private $startTime = 0;
18
public function __construct(){
19
$this->startTime = time();
20
$this->i = new wfIssues();
21
$this->wp_version = wfUtils::getWPVersion();
22
$this->apiKey = wfConfig::get('apiKey');
23
$this->api = new wfAPI($this->apiKey, $this->wp_version);
24
include('wfDict.php'); //$dictWords
25
$this->dictWords = $dictWords;
26
}
27
public function go(){
28
- $this->status(1, 'info', "Initializing scan. Memory available: " . @ini_get('memory_limit') );
29
- $this->i->deleteNew();
30
-
31
try {
32
$this->doScan();
33
if(! $this->errorStopped){
@@ -42,79 +82,60 @@ class wfScanEngine {
42
}
43
wordfence::scheduleNextScan(true);
44
}
45
public function emailNewIssues(){
46
$this->i->emailNewIssues();
47
}
48
private function doScan(){
49
- $this->status(1, 'info', "Contacting Wordfence to initiate scan");
50
- $this->api->call('log_scan', array(), array());
51
- if($this->api->errorMsg){
52
- $this->errorStop($this->api->errorMsg);
53
- return;
54
- }
55
- $unknownFiles = $this->scanKnownFiles();
56
- if($this->errorStopped){
57
- return;
58
- }
59
- if(wfConfig::get('scansEnabled_fileContents')){
60
- $this->scanFileContents($unknownFiles);
61
- if($this->errorStopped){
62
- return;
63
- }
64
- }
65
- if(wfConfig::get('scansEnabled_posts')){
66
- $this->scanPosts();
67
- if($this->errorStopped){
68
- return;
69
}
70
- }
71
- if(wfConfig::get('scansEnabled_comments')){
72
- $this->scanComments();
73
- if($this->errorStopped){ return; }
74
- }
75
- if(wfConfig::get('scansEnabled_passwds')){
76
- $this->scanAllPasswords();
77
- if($this->errorStopped){ return; }
78
- }
79
- if(wfConfig::get('scansEnabled_diskSpace')){
80
- $this->scanDiskSpace();
81
- if($this->errorStopped){ return; }
82
- }
83
- if(wfConfig::get('scansEnabled_dns')){
84
- $this->scanDNSChanges();
85
- if($this->errorStopped){ return; }
86
- }
87
- if(wfConfig::get('scansEnabled_oldVersions')){
88
- $this->scanOldVersions();
89
- if($this->errorStopped){ return; }
90
}
91
$summary = $this->i->getSummaryItems();
92
$this->status(1, 'info', "Scan Complete. Scanned " . $summary['totalFiles'] . " files, " . $summary['totalPlugins'] . " plugins, " . $summary['totalThemes'] . " themes, " . ($summary['totalPages'] + $summary['totalPosts']) . " pages, " . $summary['totalComments'] . " comments and " . $summary['totalRows'] . " records in " . (time() - $this->startTime) . " seconds.");
93
- if($this->i->totalIssues > 0){
94
$this->status(10, 'info', "SUM_FINAL:Scan complete. You have " . $this->i->totalIssues . " new issues to fix. See below for details.");
95
} else {
96
$this->status(10, 'info', "SUM_FINAL:Scan complete. Congratulations, there were no problems found.");
97
}
98
return;
99
}
100
- private function scanKnownFiles(){
101
- $malwareScanEnabled = $coreScanEnabled = $pluginScanEnabled = $themeScanEnabled = false;
102
- $statusIDX = array(
103
- 'core' => false,
104
- 'plugin' => false,
105
- 'theme' => false,
106
- 'unknown' => false
107
- );
108
if(wfConfig::get('scansEnabled_core')){
109
- $coreScanEnabled = true;
110
- $statusIDX['core'] = wordfence::statusStart("Comparing core WordPress files against originals in repository");
111
} else {
112
wordfence::statusDisabled("Skipping core scan");
113
}
114
if(wfConfig::get('isPaid')){
115
if(wfConfig::get('scansEnabled_plugins')){
116
- $pluginScanEnabled = true;
117
- $statusIDX['plugin'] = wordfence::statusStart("Premium: Comparing plugin files against originals in repository");
118
} else {
119
wordfence::statusDisabled("Skipping comparing plugin files against originals in repository");
120
}
@@ -123,8 +144,8 @@ class wfScanEngine {
123
}
124
if(wfConfig::get('isPaid')){
125
if(wfConfig::get('scansEnabled_themes')){
126
- $themeScanEnabled = true;
127
- $statusIDX['theme'] = wordfence::statusStart("Premium: Comparing theme files against originals in repository");
128
} else {
129
wordfence::statusDisabled("Skipping comparing theme files against originals in repository");
130
}
@@ -133,20 +154,19 @@ class wfScanEngine {
133
}
134
135
if(wfConfig::get('scansEnabled_malware')){
136
- $statusIDX['unknown'] = wordfence::statusStart("Scanning for known malware files");
137
- $malwareScanEnabled = true;
138
} else {
139
wordfence::statusDisabled("Skipping malware scan");
140
$this->status(2, 'info', "Skipping malware scan because it's disabled.");
141
}
142
- $summaryUpdateRequired = $this->i->summaryUpdateRequired();
143
- if((! $summaryUpdateRequired) && (! ($coreScanEnabled || $pluginScanEnabled || $themeScanEnabled || $malwareScanEnabled))){
144
$this->status(2, 'info', "Finishing this stage because we don't have to do a summary update and we don't need to do a core, plugin, theme or malware scan.");
145
return array();
146
}
147
148
//CORE SCAN
149
- $hasher = new wordfenceHash(strlen(ABSPATH));
150
$baseWPStuff = array( '.htaccess', 'index.php', 'license.txt', 'readme.html', 'wp-activate.php', 'wp-admin', 'wp-app.php', 'wp-blog-header.php', 'wp-comments-post.php', 'wp-config-sample.php', 'wp-content', 'wp-cron.php', 'wp-includes', 'wp-links-opml.php', 'wp-load.php', 'wp-login.php', 'wp-mail.php', 'wp-pass.php', 'wp-register.php', 'wp-settings.php', 'wp-signup.php', 'wp-trackback.php', 'xmlrpc.php');
151
$baseContents = scandir(ABSPATH);
152
$scanOutside = wfConfig::get('other_scanOutside');
@@ -162,13 +182,18 @@ class wfScanEngine {
162
}
163
}
164
$this->status(2, 'info', "Hashing your WordPress files for comparison against originals.");
165
- $hashes = $hasher->hashPaths(ABSPATH, $includeInScan);
166
$this->status(2, 'info', "Done hash. Updating summary items.");
167
- $this->i->updateSummaryItem('totalData', wfUtils::formatBytes($hasher->totalData));
168
- $this->i->updateSummaryItem('totalFiles', $hasher->totalFiles);
169
- $this->i->updateSummaryItem('totalDirs', $hasher->totalDirs);
170
- $this->i->updateSummaryItem('linesOfPHP', $hasher->linesOfPHP);
171
- $this->i->updateSummaryItem('linesOfJCH', $hasher->linesOfJCH);
172
173
if(! function_exists( 'get_plugins')){
174
require_once ABSPATH . '/wp-admin/includes/plugin.php';
@@ -185,7 +210,7 @@ class wfScanEngine {
185
$this->status(2, 'info', "Found " . sizeof($themes) . " themes");
186
$this->i->updateSummaryItem('totalThemes', sizeof($themes));
187
//Return now because we needed to do a summary update but don't have any other work to do.
188
- if(! ($coreScanEnabled || $pluginScanEnabled || $themeScanEnabled || $malwareScanEnabled)){
189
$this->status(2, 'info', "Finishing up because we have done our required summary update and don't need to do a core, plugin, theme or malware scan.");
190
return array();
191
}
@@ -201,13 +226,13 @@ class wfScanEngine {
201
}
202
$this->status(2, 'info', "Sending request to Wordfence servers to do main scan.");
203
$scanData = array(
204
- 'pluginScanEnabled' => $pluginScanEnabled,
205
- 'themeScanEnabled' => $themeScanEnabled,
206
- 'coreScanEnabled' => $coreScanEnabled,
207
- 'malwareScanEnabled' => $malwareScanEnabled,
208
'plugins' => $plugins,
209
'themes' => $themes,
210
- 'hashes' => wordfenceHash::bin2hex($hashes)
211
);
212
$result1 = $this->api->call('main_scan', array(), array(
213
'data' => json_encode($scanData)
@@ -241,29 +266,35 @@ class wfScanEngine {
241
}
242
}
243
foreach($haveIssues as $type => $have){
244
- if($statusIDX[$type] !== false){
245
- wordfence::statusEnd($statusIDX[$type], $have);
246
}
247
}
248
- return $result1['unknownFiles'];
249
}
250
- private function scanFileContents($unknownFiles){
251
- $statusIDX = wordfence::statusStart('Scanning file contents for infections and vulnerabilities');
252
- $statusIDX2 = wordfence::statusStart('Scanning files for URLs in Google\'s Safe Browsing List');
253
- if(! is_array($unknownFiles)){
254
- $unknownFiles = array();
255
- }
256
- $this->status(2, 'info', "Getting list of changed files since last scan.");
257
- $scanner = new wordfenceScanner($this->apiKey, $this->wp_version);
258
$this->status(2, 'info', "Starting scan of file contents");
259
- $result2 = $scanner->scan(ABSPATH, $unknownFiles);
260
$this->status(2, 'info', "Done file contents scan");
261
- if($scanner->errorMsg){
262
- $this->errorStop($scanner->errorMsg);
263
}
264
$haveIssues = false;
265
$haveIssuesGSB = false;
266
- foreach($result2 as $issue){
267
$this->status(2, 'info', "Adding issue: " . $issue['shortMsg']);
268
if($this->addIssue($issue['type'], $issue['severity'], $issue['ignoreP'], $issue['ignoreC'], $issue['shortMsg'], $issue['longMsg'], $issue['data'])){
269
if(empty($issue['data']['gsb']) === false){
@@ -273,38 +304,48 @@ class wfScanEngine {
273
}
274
}
275
}
276
- wordfence::statusEnd($statusIDX, $haveIssues);
277
- wordfence::statusEnd($statusIDX2, $haveIssuesGSB);
278
}
279
- private function scanPosts(){
280
- $statusIDX = wordfence::statusStart('Scanning posts for URL\'s in Google\'s Safe Browsing List');
281
$blogsToScan = $this->getBlogsToScan('posts');
282
$wfdb = new wfDB();
283
- $h = new wordfenceURLHoover($this->apiKey, $this->wp_version);
284
- $postDat = array();
285
foreach($blogsToScan as $blog){
286
$q1 = $wfdb->query("select ID from " . $blog['table'] . " where post_type IN ('page', 'post') and post_status = 'publish'");
287
while($idRow = mysql_fetch_assoc($q1)){
288
- $row = $wfdb->querySingleRec("select ID, post_title, post_type, post_date, post_content from " . $blog['table'] . " where ID=%d", $idRow['ID']);
289
- $h->hoover($blog['blog_id'] . '-' . $row['ID'], $row['post_title'] . ' ' . $row['post_content']);
290
- $postDat[$blog['blog_id'] . '-' . $row['ID']] = array(
291
- 'contentMD5' => md5($row['post_content']),
292
- 'title' => $row['post_title'],
293
- 'type' => $row['post_type'],
294
- 'postDate' => $row['post_date'],
295
- 'isMultisite' => $blog['isMultisite'],
296
- 'domain' => $blog['domain'],
297
- 'path' => $blog['path'],
298
- 'blog_id' => $blog['blog_id']
299
- );
300
-
301
}
302
}
303
$this->status(2, 'info', "Examining URLs found in posts we scanned for dangerous websites");
304
- $hooverResults = $h->getBaddies();
305
$this->status(2, 'info', "Done examining URls");
306
- if($h->errorMsg){
307
- $this->errorStop($h->errorMsg);
308
wordfence::statusEndErr();
309
return;
310
@@ -314,8 +355,8 @@ class wfScanEngine {
314
$arr = explode('-', $idString);
315
$blogID = $arr[0];
316
$postID = $arr[1];
317
- $uctype = ucfirst($postDat[$idString]['type']);
318
- $type = $postDat[$idString]['type'];
319
foreach($hresults as $result){
320
if($result['badList'] == 'goog-malware-shavar'){
321
$shortMsg = "$uctype contains a suspected malware URL.";
@@ -332,19 +373,19 @@ class wfScanEngine {
332
switch_to_blog($blogID);
333
}
334
$ignoreP = $idString;
335
- $ignoreC = $idString . $postDat[$idString]['contentMD5'];
336
if($this->addIssue('postBadURL', 1, $ignoreP, $ignoreC, $shortMsg, $longMsg, array(
337
'postID' => $postID,
338
'badURL' => $result['URL'],
339
- 'postTitle' => $postDat[$idString]['title'],
340
- 'type' => $postDat[$idString]['type'],
341
'uctype' => $uctype,
342
'permalink' => get_permalink($postID),
343
'editPostLink' => get_edit_post_link($postID),
344
- 'postDate' => $postDat[$idString]['postDate'],
345
- 'isMultisite' => $postDat[$idString]['isMultisite'],
346
- 'domain' => $postDat[$idString]['domain'],
347
- 'path' => $postDat[$idString]['path'],
348
'blog_id' => $blogID
349
))){
350
$haveIssues = true;
@@ -354,51 +395,16 @@ class wfScanEngine {
354
}
355
}
356
}
357
- wordfence::statusEnd($statusIDX, $haveIssues);
358
- }
359
- public function isBadComment($author, $email, $url, $IP, $content){
360
- $content = $author . ' ' . $email . ' ' . $url . ' ' . $IP . ' ' . $content;
361
- $cDesc = '';
362
- if($author){
363
- $cDesc = "Author: $author ";
364
- }
365
- if($email){
366
- $cDesc .= "Email: $email ";
367
- }
368
- $cDesc = "Source IP: $IP ";
369
- $this->status(2, 'info', "Scanning comment with $cDesc");
370
-
371
- $h = new wordfenceURLHoover($this->apiKey, $this->wp_version);
372
- $h->hoover(1, $content);
373
- $hooverResults = $h->getBaddies();
374
- if($h->errorMsg){
375
- return false;
376
- }
377
- if(sizeof($hooverResults) > 0 && isset($hooverResults[1])){
378
- $hresults = $hooverResults[1];
379
- foreach($hresults as $result){
380
- if($result['badList'] == 'goog-malware-shavar'){
381
- $this->status(2, 'info', "Marking comment as spam for containing a malware URL. Comment has $cDesc");
382
- return true;
383
- } else if($result['badList'] == 'googpub-phish-shavar'){
384
- $this->status(2, 'info', "Marking comment as spam for containing a phishing URL. Comment has $cDesc");
385
- return true;
386
- } else {
387
- //A list type that may be new and the plugin has not been upgraded yet.
388
- continue;
389
- }
390
- }
391
- }
392
- $this->status(2, 'info', "Scanned comment with $cDesc");
393
- return false;
394
}
395
- private function scanComments(){
396
- $statusIDX = wordfence::statusStart('Scanning comments for URL\'s in Google\'s Safe Browsing List');
397
- global $wpdb;
398
- $wfdb = new wfDB();
399
- $commentDat = array();
400
- $h = new wordfenceURLHoover($this->apiKey, $this->wp_version);
401
$blogsToScan = $this->getBlogsToScan('comments');
402
foreach($blogsToScan as $blog){
403
$q1 = $wfdb->query("select comment_ID from " . $blog['table'] . " where comment_approved=1");
404
if( ! $q1){
@@ -410,23 +416,34 @@ class wfScanEngine {
410
}
411
412
while($idRow = mysql_fetch_assoc($q1)){
413
- $row = $wfdb->querySingleRec("select comment_ID, comment_date, comment_type, comment_author, comment_author_url, comment_content from " . $blog['table'] . " where comment_ID=%d", $idRow['comment_ID']);
414
- $h->hoover($blog['blog_id'] . '-' . $row['comment_ID'], $row['comment_author_url'] . ' ' . $row['comment_author'] . ' ' . $row['comment_content']);
415
- $commentDat[$blog['blog_id'] . '-' . $row['comment_ID']] = array(
416
- 'contentMD5' => md5($row['comment_content'] . $row['comment_author'] . $row['comment_author_url']),
417
- 'author' => $row['comment_author'],
418
- 'type' => ($row['comment_type'] ? $row['comment_type'] : 'comment'),
419
- 'date' => $row['comment_date'],
420
- 'isMultisite' => $blog['isMultisite'],
421
- 'domain' => $blog['domain'],
422
- 'path' => $blog['path'],
423
- 'blog_id' => $blog['blog_id']
424
- );
425
}
426
}
427
- $hooverResults = $h->getBaddies();
428
- if($h->errorMsg){
429
- $this->errorStop($h->errorMsg);
430
wordfence::statusEndErr();
431
return;
432
}
@@ -435,8 +452,8 @@ class wfScanEngine {
435
$arr = explode('-', $idString);
436
$blogID = $arr[0];
437
$commentID = $arr[1];
438
- $uctype = ucfirst($commentDat[$idString]['type']);
439
- $type = $commentDat[$idString]['type'];
440
foreach($hresults as $result){
441
if($result['badList'] == 'goog-malware-shavar'){
442
$shortMsg = "$uctype contains a suspected malware URL.";
@@ -452,18 +469,18 @@ class wfScanEngine {
452
switch_to_blog($blogID);
453
}
454
$ignoreP = $idString;
455
- $ignoreC = $idString . '-' . $commentDat[$idString]['contentMD5'];
456
if($this->addIssue('commentBadURL', 1, $ignoreP, $ignoreC, $shortMsg, $longMsg, array(
457
'commentID' => $commentID,
458
'badURL' => $result['URL'],
459
- 'author' => $commentDat[$idString]['author'],
460
'type' => $type,
461
'uctype' => $uctype,
462
'editCommentLink' => get_edit_comment_link($commentID),
463
- 'commentDate' => $commentDat[$idString]['date'],
464
- 'isMultisite' => $commentDat[$idString]['isMultisite'],
465
- 'domain' => $commentDat[$idString]['domain'],
466
- 'path' => $commentDat[$idString]['path'],
467
'blog_id' => $blogID
468
))){
469
$haveIssues = true;
@@ -473,7 +490,43 @@ class wfScanEngine {
473
}
474
}
475
}
476
- wordfence::statusEnd($statusIDX, $haveIssues);
477
}
478
public function getBlogsToScan($table){
479
$wfdb = new wfDB();
@@ -518,8 +571,8 @@ class wfScanEngine {
518
}
519
return false;
520
}
521
- private function scanAllPasswords(){
522
- $statusIDX = wordfence::statusStart('Scanning for weak passwords');
523
global $wpdb;
524
$ws = $wpdb->get_results("SELECT ID, user_login FROM $wpdb->users");
525
$haveIssues = false;
@@ -528,11 +581,11 @@ class wfScanEngine {
528
$isWeak = $this->scanUserPassword($user->ID);
529
if($isWeak){ $haveIssues = true; }
530
}
531
- wordfence::statusEnd($statusIDX, $haveIssues);
532
}
533
public function scanUserPassword($userID){
534
require_once( ABSPATH . 'wp-includes/class-phpass.php');
535
- $hasher = new PasswordHash(8, TRUE);
536
$userDat = get_userdata($userID);
537
$this->status(2, 'info', "Checking password strength of user '" . $userDat->user_login . "'");
538
$shortMsg = "";
@@ -552,7 +605,7 @@ class wfScanEngine {
552
}
553
$haveIssue = false;
554
for($i = 0; $i < sizeof($words); $i++){
555
- if($hasher->CheckPassword($words[$i], $userDat->user_pass)){
556
$this->status(2, 'info', "Adding issue " . $shortMsg);
557
if($this->addIssue('easyPassword', $level, $userDat->ID, $userDat->ID . '-' . $userDat->user_pass, $shortMsg, $longMsg, array(
558
'ID' => $userDat->ID,
@@ -570,13 +623,13 @@ class wfScanEngine {
570
$this->status(2, 'info', "Completed checking password strength of user '" . $userDat->user_login . "'");
571
return $haveIssue;
572
}
573
- private function scanDiskSpace(){
574
- $statusIDX = wordfence::statusStart("Scanning to check available disk space");
575
$total = disk_total_space('.');
576
$free = disk_free_space('.');
577
$this->status(2, 'info', "Total space: $total Free space: $free");
578
if( (! $total) || (! $free )){ //If we get zeros it's probably not reading right. If free is zero then we're out of space and already in trouble.
579
- wordfence::statusEnd($statusIDX, false);
580
return;
581
}
582
$level = false;
@@ -587,22 +640,22 @@ class wfScanEngine {
587
} else if($spaceLeft < 1.5){
588
$level = 2;
589
} else {
590
- wordfence::statusEnd($statusIDX, false);
591
return;
592
}
593
if($this->addIssue('diskSpace', $level, 'diskSpace' . $level, 'diskSpace' . $level, "You have $spaceLeft" . "% disk space remaining", "You only have $spaceLeft" . "% of your disk space remaining. Please free up disk space or your website may stop serving requests.", array(
594
'spaceLeft' => $spaceLeft ))){
595
- wordfence::statusEnd($statusIDX, true);
596
} else {
597
- wordfence::statusEnd($statusIDX, false);
598
}
599
}
600
- private function scanDNSChanges(){
601
if(! function_exists('dns_get_record')){
602
$this->status(1, 'info', "Skipping DNS scan because this system does not support dns_get_record()");
603
return;
604
}
605
- $statusIDX = wordfence::statusStart("Scanning DNS for unauthorized changes");
606
$haveIssues = false;
607
$home = get_home_url();
608
if(preg_match('/https?:\/\/([^\/]+)/i', $home, $matches)){
@@ -694,10 +747,10 @@ class wfScanEngine {
694
695
wfConfig::set('wf_dnsLogged', 1);
696
}
697
- wordfence::statusEnd($statusIDX, $haveIssues);
698
}
699
- private function scanOldVersions(){
700
- $statusIDX = wordfence::statusStart("Scanning for old themes, plugins and core files");
701
if(! function_exists( 'get_preferred_from_update_core')){
702
require_once(ABSPATH . 'wp-admin/includes/update.php');
703
}
@@ -753,7 +806,7 @@ class wfScanEngine {
753
754
}
755
}
756
- wordfence::statusEnd($statusIDX, $haveIssues);
757
}
758
private function errorStop($msg){
759
$this->errorStopped = true;
@@ -766,6 +819,125 @@ class wfScanEngine {
766
private function addIssue($type, $severity, $ignoreP, $ignoreC, $shortMsg, $longMsg, $templateData){
767
return $this->i->addIssue($type, $severity, $ignoreP, $ignoreC, $shortMsg, $longMsg, $templateData);
768
}
769
}
770
771
?>
7
require_once('wfDB.php');
8
require_once('wfUtils.php');
9
class wfScanEngine {
10
private $api = false;
11
+ private $dictWords = array();
12
+
13
+ //Beginning of serialized properties on sleep
14
+ private $hasher = false;
15
+ private $hashes = false;
16
+ private $jobList = array();
17
+ private $i = false;
18
private $wp_version = false;
19
private $apiKey = false;
20
private $errorStopped = false;
21
private $startTime = 0;
22
+ private $scanStep = 0;
23
+ private $maxExecTime = 10; //If more than $maxExecTime has elapsed since last check, fork a new scan process and continue
24
+ private $malwareScanEnabled = false;
25
+ private $pluginScanEnabled = false;
26
+ private $coreScanEnabled = false;
27
+ private $themeScanEnabled = false;
28
+ private $unknownFiles = array();
29
+ private $fileContentsResults = false;
30
+ private $scanner = false;
31
+ private $scanQueue = array();
32
+ private $hoover = false;
33
+ private $scanData = array();
34
+ private $statusIDX = array(
35
+ 'core' => false,
36
+ 'plugin' => false,
37
+ 'theme' => false,
38
+ 'unknown' => false
39
+ );
40
+ public function __sleep(){ //Same order here as above for properties that are included in serialization
41
+ return array('hasher', 'hashes', 'jobList', 'i', 'wp_version', 'apiKey', 'errorStopped', 'startTime', 'scanStep', 'maxExecTime', 'malwareScanEnabled', 'pluginScanEnabled', 'coreScanEnabled', 'themeScanEnabled', 'unknownFiles', 'fileContentsResults', 'scanner', 'scanQueue', 'hoover', 'scanData', 'statusIDX');
42
+ }
43
public function __construct(){
44
$this->startTime = time();
45
$this->i = new wfIssues();
46
+ $this->i->deleteNew();
47
+ $this->cycleStartTime = time();
48
$this->wp_version = wfUtils::getWPVersion();
49
$this->apiKey = wfConfig::get('apiKey');
50
$this->api = new wfAPI($this->apiKey, $this->wp_version);
51
include('wfDict.php'); //$dictWords
52
$this->dictWords = $dictWords;
53
+ foreach(array('init', 'main', 'finish') as $op){ $this->jobList[] = 'knownFiles_' . $op; };
54
+ foreach(array('fileContents', 'posts', 'comments', 'passwds', 'dns', 'diskSpace', 'oldVersions') as $scanType){
55
+ if(wfConfig::get('scansEnabled_' . $scanType)){
56
+ if(method_exists($this, 'scan_' . $scanType . '_init')){
57
+ foreach(array('init', 'main', 'finish') as $op){ $this->jobList[] = $scanType . '_' . $op; };
58
+ } else {
59
+ $this->jobList[] = $scanType;
60
+ }
61
+ }
62
+ }
63
+ }
64
+ public function __wakeup(){
65
+ $this->cycleStartTime = time();
66
+ $this->api = new wfAPI($this->apiKey, $this->wp_version);
67
+ include('wfDict.php'); //$dictWords
68
+ $this->dictWords = $dictWords;
69
}
70
public function go(){
71
try {
72
$this->doScan();
73
if(! $this->errorStopped){
82
}
83
wordfence::scheduleNextScan(true);
84
}
85
+ public function forkIfNeeded(){
86
+ if(time() - $this->cycleStartTime > $this->maxExecTime){
87
+ wordfence::status(2, 'info', "Forking during hash scan to ensure continuity.");
88
+ $this->fork();
89
+ }
90
+ }
91
+ public function fork(){
92
+ wfConfig::set_ser('wfsd_engine', $this);
93
+ wfUtils::clearScanLock();
94
+ self::startScan(true);
95
+ exit(0);
96
+ }
97
public function emailNewIssues(){
98
$this->i->emailNewIssues();
99
}
100
private function doScan(){
101
+ while(sizeof($this->jobList) > 0){
102
+ $jobName = $this->jobList[0];
103
+ call_user_func(array($this, 'scan_' . $jobName));
104
+ array_shift($this->jobList); //only shift once we're done because we may pause halfway through a job and need to pick up where we left off
105
+ if($this->errorStopped){
106
+ return;
107
}
108
+ $this->fork();
109
}
110
$summary = $this->i->getSummaryItems();
111
$this->status(1, 'info', "Scan Complete. Scanned " . $summary['totalFiles'] . " files, " . $summary['totalPlugins'] . " plugins, " . $summary['totalThemes'] . " themes, " . ($summary['totalPages'] + $summary['totalPosts']) . " pages, " . $summary['totalComments'] . " comments and " . $summary['totalRows'] . " records in " . (time() - $this->startTime) . " seconds.");
112
+ if($this->i->totalIssues > 0){
113
$this->status(10, 'info', "SUM_FINAL:Scan complete. You have " . $this->i->totalIssues . " new issues to fix. See below for details.");
114
} else {
115
$this->status(10, 'info', "SUM_FINAL:Scan complete. Congratulations, there were no problems found.");
116
}
117
return;
118
}
119
+ public function getCurrentJob(){
120
+ return $this->jobList[0];
121
+ }
122
+ private function scan_knownFiles_init(){
123
+ $this->status(1, 'info', "Contacting Wordfence to initiate scan");
124
+ $this->api->call('log_scan', array(), array());
125
+ if($this->api->errorMsg){
126
+ $this->errorStop($this->api->errorMsg);
127
+ return;
128
+ }
129
if(wfConfig::get('scansEnabled_core')){
130
+ $this->coreScanEnabled = true;
131
+ $this->statusIDX['core'] = wordfence::statusStart("Comparing core WordPress files against originals in repository");
132
} else {
133
wordfence::statusDisabled("Skipping core scan");
134
}
135
if(wfConfig::get('isPaid')){
136
if(wfConfig::get('scansEnabled_plugins')){
137
+ $this->pluginScanEnabled = true;
138
+ $this->statusIDX['plugin'] = wordfence::statusStart("Premium: Comparing plugin files against originals in repository");
139
} else {
140
wordfence::statusDisabled("Skipping comparing plugin files against originals in repository");
141
}
144
}
145
if(wfConfig::get('isPaid')){
146
if(wfConfig::get('scansEnabled_themes')){
147
+ $this->themeScanEnabled = true;
148
+ $this->statusIDX['theme'] = wordfence::statusStart("Premium: Comparing theme files against originals in repository");
149
} else {
150
wordfence::statusDisabled("Skipping comparing theme files against originals in repository");
151
}
154
}
155
156
if(wfConfig::get('scansEnabled_malware')){
157
+ $this->statusIDX['unknown'] = wordfence::statusStart("Scanning for known malware files");
158
+ $this->malwareScanEnabled = true;
159
} else {
160
wordfence::statusDisabled("Skipping malware scan");
161
$this->status(2, 'info', "Skipping malware scan because it's disabled.");
162
}
163
+ if((! $this->i->summaryUpdateRequired()) && (! ($this->coreScanEnabled || $this->pluginScanEnabled || $this->themeScanEnabled || $this->malwareScanEnabled))){
164
$this->status(2, 'info', "Finishing this stage because we don't have to do a summary update and we don't need to do a core, plugin, theme or malware scan.");
165
return array();
166
}
167
168
//CORE SCAN
169
+ $this->hasher = new wordfenceHash(strlen(ABSPATH));
170
$baseWPStuff = array( '.htaccess', 'index.php', 'license.txt', 'readme.html', 'wp-activate.php', 'wp-admin', 'wp-app.php', 'wp-blog-header.php', 'wp-comments-post.php', 'wp-config-sample.php', 'wp-content', 'wp-cron.php', 'wp-includes', 'wp-links-opml.php', 'wp-load.php', 'wp-login.php', 'wp-mail.php', 'wp-pass.php', 'wp-register.php', 'wp-settings.php', 'wp-signup.php', 'wp-trackback.php', 'xmlrpc.php');
171
$baseContents = scandir(ABSPATH);
172
$scanOutside = wfConfig::get('other_scanOutside');
182
}
183
}
184
$this->status(2, 'info', "Hashing your WordPress files for comparison against originals.");
185
+ $this->hasher->buildFileQueue(ABSPATH, $includeInScan);
186
+ }
187
+ private function scan_knownFiles_main(){
188
+ $this->hashes = $this->hasher->genHashes($this);
189
+ }
190
+ private function scan_knownFiles_finish(){
191
$this->status(2, 'info', "Done hash. Updating summary items.");
192
+ $this->i->updateSummaryItem('totalData', wfUtils::formatBytes($this->hasher->totalData));
193
+ $this->i->updateSummaryItem('totalFiles', $this->hasher->totalFiles);
194
+ $this->i->updateSummaryItem('totalDirs', $this->hasher->totalDirs);
195
+ $this->i->updateSummaryItem('linesOfPHP', $this->hasher->linesOfPHP);
196
+ $this->i->updateSummaryItem('linesOfJCH', $this->hasher->linesOfJCH);
197
198
if(! function_exists( 'get_plugins')){
199
require_once ABSPATH . '/wp-admin/includes/plugin.php';
210
$this->status(2, 'info', "Found " . sizeof($themes) . " themes");
211
$this->i->updateSummaryItem('totalThemes', sizeof($themes));
212
//Return now because we needed to do a summary update but don't have any other work to do.
213
+ if(! ($this->coreScanEnabled || $this->pluginScanEnabled || $this->themeScanEnabled || $this->malwareScanEnabled)){
214
$this->status(2, 'info', "Finishing up because we have done our required summary update and don't need to do a core, plugin, theme or malware scan.");
215
return array();
216
}
226
}
227
$this->status(2, 'info', "Sending request to Wordfence servers to do main scan.");
228
$scanData = array(
229
+ 'pluginScanEnabled' => $this->pluginScanEnabled,
230
+ 'themeScanEnabled' => $this->themeScanEnabled,
231
+ 'coreScanEnabled' => $this->coreScanEnabled,
232
+ 'malwareScanEnabled' => $this->malwareScanEnabled,
233
'plugins' => $plugins,
234
'themes' => $themes,
235
+ 'hashes' => wordfenceHash::bin2hex($this->hashes)
236
);
237
$result1 = $this->api->call('main_scan', array(), array(
238
'data' => json_encode($scanData)
266
}
267
}
268
foreach($haveIssues as $type => $have){
269
+ if($this->statusIDX[$type] !== false){
270
+ wordfence::statusEnd($this->statusIDX[$type], $have);
271
}
272
}
273
+
274
+ $this->unknownFiles = $result1['unknownFiles'];
275
}
276
+ private function scan_fileContents_init(){
277
+ $this->statusIDX['infect'] = wordfence::statusStart('Scanning file contents for infections and vulnerabilities');
278
+ $this->statusIDX['GSB'] = wordfence::statusStart('Scanning files for URLs in Google\'s Safe Browsing List');
279
+ if(! is_array($this->unknownFiles)){
280
+ $this->unknownFiles = array();
281
+ }
282
+ $this->scanner = new wordfenceScanner($this->apiKey, $this->wp_version, $this->unknownFiles, ABSPATH);
283
+ $this->unknownFiles = false;
284
$this->status(2, 'info', "Starting scan of file contents");
285
+ }
286
+ private function scan_fileContents_main(){
287
+ $this->fileContentsResults = $this->scanner->scan($this);
288
+ }
289
+ private function scan_fileContents_finish(){
290
$this->status(2, 'info', "Done file contents scan");
291
+ if($this->scanner->errorMsg){
292
+ $this->errorStop($this->scanner->errorMsg);
293
}
294
+ $this->scanner = null;
295
$haveIssues = false;
296
$haveIssuesGSB = false;
297
+ foreach($this->fileContentsResults as $issue){
298
$this->status(2, 'info', "Adding issue: " . $issue['shortMsg']);
299
if($this->addIssue($issue['type'], $issue['severity'], $issue['ignoreP'], $issue['ignoreC'], $issue['shortMsg'], $issue['longMsg'], $issue['data'])){
300
if(empty($issue['data']['gsb']) === false){
304
}
305
}
306
}
307
+ $this->fileContentsResults = null;
308
+ wordfence::statusEnd($this->statusIDX['infect'], $haveIssues);
309
+ wordfence::statusEnd($this->statusIDX['GSB'], $haveIssuesGSB);
310
}
311
+ private function scan_posts_init(){
312
+ $this->statusIDX['posts'] = wordfence::statusStart('Scanning posts for URL\'s in Google\'s Safe Browsing List');
313
$blogsToScan = $this->getBlogsToScan('posts');
314
$wfdb = new wfDB();
315
+ $this->hoover = new wordfenceURLHoover($this->apiKey, $this->wp_version);
316
foreach($blogsToScan as $blog){
317
$q1 = $wfdb->query("select ID from " . $blog['table'] . " where post_type IN ('page', 'post') and post_status = 'publish'");
318
while($idRow = mysql_fetch_assoc($q1)){
319
+ $this->scanQueue[] = array($blog, $idRow['ID']);
320
}
321
}
322
+ }
323
+ private function scan_posts_main(){
324
+ $wfdb = new wfDB();
325
+ while($elem = array_shift($this->scanQueue)){
326
+ $blog = $elem[0];
327
+ $postID = $elem[1];
328
+ $row = $wfdb->querySingleRec("select ID, post_title, post_type, post_date, post_content from " . $blog['table'] . " where ID=%d", $postID);
329
+ $this->hoover->hoover($blog['blog_id'] . '-' . $row['ID'], $row['post_title'] . ' ' . $row['post_content']);
330
+ $this->scanData[$blog['blog_id'] . '-' . $row['ID']] = array(
331
+ 'contentMD5' => md5($row['post_content']),
332
+ 'title' => $row['post_title'],
333
+ 'type' => $row['post_type'],
334
+ 'postDate' => $row['post_date'],
335
+ 'isMultisite' => $blog['isMultisite'],
336
+ 'domain' => $blog['domain'],
337
+ 'path' => $blog['path'],
338
+ 'blog_id' => $blog['blog_id']
339
+ );
340
+ $this->forkIfNeeded();
341
+ }
342
+ }
343
+ private function scan_posts_finish(){
344
$this->status(2, 'info', "Examining URLs found in posts we scanned for dangerous websites");
345
+ $hooverResults = $this->hoover->getBaddies();
346
$this->status(2, 'info', "Done examining URls");
347
+ if($this->hoover->errorMsg){
348
+ $this->errorStop($this->hoover->errorMsg);
349
wordfence::statusEndErr();
350
return;
351
355
$arr = explode('-', $idString);
356
$blogID = $arr[0];
357
$postID = $arr[1];
358
+ $uctype = ucfirst($this->scanData[$idString]['type']);
359
+ $type = $this->scanData[$idString]['type'];
360
foreach($hresults as $result){
361
if($result['badList'] == 'goog-malware-shavar'){
362
$shortMsg = "$uctype contains a suspected malware URL.";
373
switch_to_blog($blogID);
374
}
375
$ignoreP = $idString;
376
+ $ignoreC = $idString . $this->scanData[$idString]['contentMD5'];
377
if($this->addIssue('postBadURL', 1, $ignoreP, $ignoreC, $shortMsg, $longMsg, array(
378
'postID' => $postID,
379
'badURL' => $result['URL'],
380
+ 'postTitle' => $this->scanData[$idString]['title'],
381
+ 'type' => $this->scanData[$idString]['type'],
382
'uctype' => $uctype,
383
'permalink' => get_permalink($postID),
384
'editPostLink' => get_edit_post_link($postID),
385
+ 'postDate' => $this->scanData[$idString]['postDate'],
386
+ 'isMultisite' => $this->scanData[$idString]['isMultisite'],
387
+ 'domain' => $this->scanData[$idString]['domain'],
388
+ 'path' => $this->scanData[$idString]['path'],
389
'blog_id' => $blogID
390
))){
391
$haveIssues = true;
395
}
396
}
397
}
398
+ $this->scanData = array();
399
+ wordfence::statusEnd($this->statusIDX['posts'], $haveIssues);
400
}
401
+ private function scan_comments_init(){
402
+ $this->statusIDX['comments'] = wordfence::statusStart('Scanning comments for URL\'s in Google\'s Safe Browsing List');
403
+ $this->scanData = array();
404
+ $this->scanQueue = array();
405
+ $this->hoover = new wordfenceURLHoover($this->apiKey, $this->wp_version);
406
$blogsToScan = $this->getBlogsToScan('comments');
407
+ $wfdb = new wfDB();
408
foreach($blogsToScan as $blog){
409
$q1 = $wfdb->query("select comment_ID from " . $blog['table'] . " where comment_approved=1");
410
if( ! $q1){
416
}
417
418
while($idRow = mysql_fetch_assoc($q1)){
419
+ $this->scanQueue[] = array($blog, $idRow['comment_ID']);
420
}
421
}
422
+ }
423
+ private function scan_comments_main(){
424
+ $wfdb = new wfDB();
425
+ while($elem = array_shift($this->scanQueue)){
426
+ $blog = $elem[0];
427
+ $commentID = $elem[1];
428
+ $row = $wfdb->querySingleRec("select comment_ID, comment_date, comment_type, comment_author, comment_author_url, comment_content from " . $blog['table'] . " where comment_ID=%d", $commentID);
429
+ $this->hoover->hoover($blog['blog_id'] . '-' . $row['comment_ID'], $row['comment_author_url'] . ' ' . $row['comment_author'] . ' ' . $row['comment_content']);
430
+ $this->scanData[$blog['blog_id'] . '-' . $row['comment_ID']] = array(
431
+ 'contentMD5' => md5($row['comment_content'] . $row['comment_author'] . $row['comment_author_url']),
432
+ 'author' => $row['comment_author'],
433
+ 'type' => ($row['comment_type'] ? $row['comment_type'] : 'comment'),
434
+ 'date' => $row['comment_date'],
435
+ 'isMultisite' => $blog['isMultisite'],
436
+ 'domain' => $blog['domain'],
437
+ 'path' => $blog['path'],
438
+ 'blog_id' => $blog['blog_id']
439
+ );
440
+ $this->forkIfNeeded();
441
+ }
442
+ }
443
+ private function scan_comments_finish(){
444
+ $hooverResults = $this->hoover->getBaddies();
445
+ if($this->hoover->errorMsg){
446
+ $this->errorStop($this->hoover->errorMsg);
447
wordfence::statusEndErr();
448
return;
449
}
452
$arr = explode('-', $idString);
453
$blogID = $arr[0];
454
$commentID = $arr[1];
455
+ $uctype = ucfirst($this->scanData[$idString]['type']);
456
+ $type = $this->scanData[$idString]['type'];
457
foreach($hresults as $result){
458
if($result['badList'] == 'goog-malware-shavar'){
459
$shortMsg = "$uctype contains a suspected malware URL.";
469
switch_to_blog($blogID);
470
}
471
$ignoreP = $idString;
472
+ $ignoreC = $idString . '-' . $this->scanData[$idString]['contentMD5'];
473
if($this->addIssue('commentBadURL', 1, $ignoreP, $ignoreC, $shortMsg, $longMsg, array(
474
'commentID' => $commentID,
475
'badURL' => $result['URL'],
476
+ 'author' => $this->scanData[$idString]['author'],
477
'type' => $type,
478
'uctype' => $uctype,
479
'editCommentLink' => get_edit_comment_link($commentID),
480
+ 'commentDate' => $this->scanData[$idString]['date'],
481
+ 'isMultisite' => $this->scanData[$idString]['isMultisite'],
482
+ 'domain' => $this->scanData[$idString]['domain'],
483
+ 'path' => $this->scanData[$idString]['path'],
484
'blog_id' => $blogID
485
))){
486
$haveIssues = true;
490
}
491
}
492
}
493
+ wordfence::statusEnd($this->statusIDX['comments'], $haveIssues);
494
+ }
495
+ public function isBadComment($author, $email, $url, $IP, $content){
496
+ $content = $author . ' ' . $email . ' ' . $url . ' ' . $IP . ' ' . $content;
497
+ $cDesc = '';
498
+ if($author){
499
+ $cDesc = "Author: $author ";
500
+ }
501
+ if($email){
502
+ $cDesc .= "Email: $email ";
503
+ }
504
+ $cDesc = "Source IP: $IP ";
505
+ $this->status(2, 'info', "Scanning comment with $cDesc");
506
+
507
+ $h = new wordfenceURLHoover($this->apiKey, $this->wp_version);
508
+ $h->hoover(1, $content);
509
+ $hooverResults = $h->getBaddies();
510
+ if($h->errorMsg){
511
+ return false;
512
+ }
513
+ if(sizeof($hooverResults) > 0 && isset($hooverResults[1])){
514
+ $hresults = $hooverResults[1];
515
+ foreach($hresults as $result){
516
+ if($result['badList'] == 'goog-malware-shavar'){
517
+ $this->status(2, 'info', "Marking comment as spam for containing a malware URL. Comment has $cDesc");
518
+ return true;
519
+ } else if($result['badList'] == 'googpub-phish-shavar'){
520
+ $this->status(2, 'info', "Marking comment as spam for containing a phishing URL. Comment has $cDesc");
521
+ return true;
522
+ } else {
523
+ //A list type that may be new and the plugin has not been upgraded yet.
524
+ continue;
525
+ }
526
+ }
527
+ }
528
+ $this->status(2, 'info', "Scanned comment with $cDesc");
529
+ return false;
530
}
531
public function getBlogsToScan($table){
532
$wfdb = new wfDB();
571
}
572
return false;
573
}
574
+ private function scan_passwds(){
575
+ $this->statusIDX['passwds'] = wordfence::statusStart('Scanning for weak passwords');
576
global $wpdb;
577
$ws = $wpdb->get_results("SELECT ID, user_login FROM $wpdb->users");
578
$haveIssues = false;
581
$isWeak = $this->scanUserPassword($user->ID);
582
if($isWeak){ $haveIssues = true; }
583
}
584
+ wordfence::statusEnd($this->statusIDX['passwds'], $haveIssues);
585
}
586
public function scanUserPassword($userID){
587
require_once( ABSPATH . 'wp-includes/class-phpass.php');
588
+ $passwdHasher = new PasswordHash(8, TRUE);
589
$userDat = get_userdata($userID);
590
$this->status(2, 'info', "Checking password strength of user '" . $userDat->user_login . "'");
591
$shortMsg = "";
605
}
606
$haveIssue = false;
607
for($i = 0; $i < sizeof($words); $i++){
608
+ if($passwdHasher->CheckPassword($words[$i], $userDat->user_pass)){
609
$this->status(2, 'info', "Adding issue " . $shortMsg);
610
if($this->addIssue('easyPassword', $level, $userDat->ID, $userDat->ID . '-' . $userDat->user_pass, $shortMsg, $longMsg, array(
611
'ID' => $userDat->ID,
623
$this->status(2, 'info', "Completed checking password strength of user '" . $userDat->user_login . "'");
624
return $haveIssue;
625
}
626
+ private function scan_diskSpace(){
627
+ $this->statusIDX['diskSpace'] = wordfence::statusStart("Scanning to check available disk space");
628
$total = disk_total_space('.');
629
$free = disk_free_space('.');
630
$this->status(2, 'info', "Total space: $total Free space: $free");
631
if( (! $total) || (! $free )){ //If we get zeros it's probably not reading right. If free is zero then we're out of space and already in trouble.
632
+ wordfence::statusEnd($this->statusIDX['diskSpace'], false);
633
return;
634
}
635
$level = false;
640
} else if($spaceLeft < 1.5){
641
$level = 2;
642
} else {
643
+ wordfence::statusEnd($this->statusIDX['diskSpace'], false);
644
return;
645
}
646
if($this->addIssue('diskSpace', $level, 'diskSpace' . $level, 'diskSpace' . $level, "You have $spaceLeft" . "% disk space remaining", "You only have $spaceLeft" . "% of your disk space remaining. Please free up disk space or your website may stop serving requests.", array(
647
'spaceLeft' => $spaceLeft ))){
648
+ wordfence::statusEnd($this->statusIDX['diskSpace'], true);
649
} else {
650
+ wordfence::statusEnd($this->statusIDX['diskSpace'], false);
651
}
652
}
653
+ private function scan_dns(){
654
if(! function_exists('dns_get_record')){
655
$this->status(1, 'info', "Skipping DNS scan because this system does not support dns_get_record()");
656
return;
657
}
658
+ $this->statusIDX['dns'] = wordfence::statusStart("Scanning DNS for unauthorized changes");
659
$haveIssues = false;
660
$home = get_home_url();
661
if(preg_match('/https?:\/\/([^\/]+)/i', $home, $matches)){
747
748
wfConfig::set('wf_dnsLogged', 1);
749
}
750
+ wordfence::statusEnd($this->statusIDX['dns'], $haveIssues);
751
}
752
+ private function scan_oldVersions(){
753
+ $this->statusIDX['oldVersions'] = wordfence::statusStart("Scanning for old themes, plugins and core files");
754
if(! function_exists( 'get_preferred_from_update_core')){
755
require_once(ABSPATH . 'wp-admin/includes/update.php');
756
}
806
807
}
808
}
809
+ wordfence::statusEnd($this->statusIDX['oldVersions'], $haveIssues);
810
}
811
private function errorStop($msg){
812
$this->errorStopped = true;
819
private function addIssue($type, $severity, $ignoreP, $ignoreC, $shortMsg, $longMsg, $templateData){
820
return $this->i->addIssue($type, $severity, $ignoreP, $ignoreC, $shortMsg, $longMsg, $templateData);
821
}
822
+ public static function startScan($isFork = false){
823
+ wordfence::status(4, 'info', "Entering start scan routine");
824
+ if(wfUtils::isScanRunning()){
825
+ return "A scan is already running.";
826
+ }
827
+
828
+ $cron_url = plugins_url('wordfence/wfscan.php?isFork=' . ($isFork ? '1' : '0'));
829
+ wordfence::status(4, 'info', "Cron URL is: " . $cron_url);
830
+ $cronKey = wfUtils::bigRandomHex();
831
+ wordfence::status(4, 'info', "cronKey is: " . $cronKey);
832
+ wfConfig::set('currentCronKey', time() . ',' . $cronKey);
833
+ wordfence::status(4, 'info', "cronKey is set");
834
+ $result = wp_remote_post( $cron_url, array(
835
+ 'timeout' => 0.5,
836
+ 'blocking' => true,
837
+ 'sslverify' => false,
838
+ 'headers' => array(
839
+ 'x-wordfence-cronkey' => $cronKey
840
+ )
841
+ ) );
842
+ $procResp = self::processResponse($result);
843
+ if($procResp){ return $procResp; }
844
+ //If the currentCronKey was eaten, then cron executed so return
845
+ wfConfig::clearCache(); if(! wfConfig::get('currentCronKey')){
846
+ wordfence::status(4, 'info', "cronkey is empty so cron executed. Returning.");
847
+ return false;
848
+ }
849
+
850
+
851
+ //This request is for hosts that don't know their own name. i.e. they don't have example.com in their hosts file or DNS pointing to their own IP address or loopback address. So we throw a hail mary to loopback.
852
+ wordfence::status(4, 'info', "cronkey is still set so sleeping for 0.2 seconds and checking again before trying another approach");
853
+ usleep(200000);
854
+ wfConfig::clearCache();
855
+ if(wfConfig::get('currentCronKey')){ //cron key is still set, so cron hasn't executed yet. Maybe the request didn't go through
856
+ wordfence::status(4, 'info', "cronkey is still set so about to manually set host header and try again");
857
+ $cron_url2 = preg_replace('/^(https?):\/\/[^\/]+/', '$1://127.0.0.1', $cron_url);
858
+ wordfence::status(4, 'info', "cron url is: $cron_url2");
859
+ $siteURL = site_url();
860
+ wordfence::status(4, 'info', "siteURL is: $siteURL");
861
+ if(preg_match('/^https?:\/\/([^\/]+)/i', site_url(), $matches)){
862
+ $host = $matches[1];
863
+ wordfence::status(4, 'info', "Extracted host $host from siteURL and trying remote post with manual host header set.");
864
+ $result = wp_remote_post( $cron_url2, array(
865
+ 'timeout' => 0.5,
866
+ 'blocking' => true,
867
+ 'sslverify' => false,
868
+ 'headers' => array(
869
+ 'x-wordfence-cronkey' => $cronKey,
870
+ 'Host' => $host
871
+ )
872
+ ) );
873
+ $procResp = self::processResponse($result);
874
+ if($procResp){ return $procResp; }
875
+ }
876
+ } else {
877
+ return false;
878
+ }
879
+
880
+ //Try again with longer timeout. Dreamhost was giving DNS lookup timeouts
881
+ usleep(200000);
882
+ wfConfig::clearCache();
883
+ if(wfConfig::get('currentCronKey')){
884
+ wordfence::status(4, "info", "cronkey is still set. Trying hostname again with longer timeout.");
885
+ $result = wp_remote_post( $cron_url, array(
886
+ 'timeout' => 10,
887
+ 'blocking' => true,
888
+ 'sslverify' => false,
889
+ 'headers' => array(
890
+ 'x-wordfence-cronkey' => $cronKey
891
+ )
892
+ ) );
893
+ $procResp = self::processResponse($result);
894
+ if($procResp){ return $procResp; }
895
+ //If the currentCronKey was eaten, then cron executed so return
896
+ wfConfig::clearCache(); if(! wfConfig::get('currentCronKey')){
897
+ wordfence::status(4, 'info', "cronkey is empty so cron executed. Returning.");
898
+ return false;
899
+ }
900
+ }
901
+ return false;
902
+ }
903
+ public function processResponse($result){
904
+ if(is_wp_error($result)){
905
+ wordfence::status(4, 'info', "Debug output of WP_Error: " . implode('; ', $result->get_error_messages()) );
906
+ } else {
907
+ if(is_array($result)){
908
+ if(is_array($result['response'])){
909
+ wordfence::status(4, 'info', "POST response: " . $result['response']['code'] . ' ' . $result['response']['message']);
910
+ }
911
+ wordfence::status(4, 'info', "POST response body: " . $result['body']);
912
+ }
913
+ }
914
+ if((! is_wp_error($result)) && is_array($result) && empty($result['body']) === false){
915
+ if(strpos($result['body'], 'WFSOURCEVISIBLE') !== false){
916
+ wordfence::status(4, 'info', "wfscan.php source is visible.");
917
+ $msg = "Wordfence can't run because the source code of your WordPress plugin files is visible from the Internet. This is a serious security risk which you need to fix. Please look for .htaccess files in your WordPress root directory and your wp-content/ and wp-content/plugins/ directories that may contain malicious code designed to reveal your site source code to a hacker.";
918
+ $htfiles = array();
919
+ if(file_exists(ABSPATH . 'wp-content/.htaccess')){
920
+ array_push($htfiles, '<a href="' . wfUtils::getSiteBaseURL() . '?_wfsf=view&nonce=' . wp_create_nonce('wp-ajax') . '&file=wp-content/.htaccess" target="_blank">wp-content/.htaccess</a>');
921
+ }
922
+ if(file_exists(ABSPATH . 'wp-content/plugins/.htaccess')){
923
+ array_push($htfiles, '<a href="' . wfUtils::getSiteBaseURL() . '?_wfsf=view&nonce=' . wp_create_nonce('wp-ajax') . '&file=wp-content/plugins/.htaccess" target="_blank">wp-content/plugins/.htaccess</a>');
924
+ }
925
+ if(sizeof($htfiles) > 0){
926
+ $msg .= "<br /><br />Click to view the .htaccess files below that may be the cause of this problem:<br />" . implode('<br />', $htfiles);
927
+ }
928
+ return $msg;
929
+
930
+ } else if(strpos($result['body'], '{') !== false && strpos($result['body'], 'errorMsg') !== false){
931
+ wordfence::status(4, 'info', "Got response from cron containing json");
932
+ $resp = json_decode($result['body'], true);
933
+ if(empty($resp['errorMsg']) === false){
934
+ wordfence::status(4, 'info', "Got an error message from cron: " . $resp['errorMsg']);
935
+ return $resp['errorMsg'];
936
+ }
937
+ }
938
+ }
939
+ return false;
940
+ }
941
}
942
943
?>
lib/wfUtils.php CHANGED
@@ -1,6 +1,7 @@
1
<?php
2
class wfUtils {
3
- private static $reverseLookupCache = array();
4
public static function makeTimeAgo($secs, $noSeconds = false) {
5
if($secs < 1){
6
return "a moment";
@@ -66,7 +67,8 @@ class wfUtils {
66
return plugins_url() . '/wordfence/';
67
}
68
public static function getPluginBaseDir(){
69
- return ABSPATH . 'wp-content/plugins/';
70
}
71
public static function getIP(){
72
$ip = 0;
@@ -81,18 +83,7 @@ class wfUtils {
81
public static function getRequestedURL(){
82
return ($_SERVER['HTTPS'] ? 'https' : 'http') . '://' . $_SERVER['SERVER_NAME'] . $_SERVER['REQUEST_URI'];
83
}
84
- public static function reverseLookup($IP){
85
- if(! isset(self::$reverseLookupCache[$IP])){
86
- $ptr = implode(".", array_reverse(explode(".",$IP))) . ".in-addr.arpa";
87
- $host = dns_get_record($ptr, DNS_PTR);
88
- if($host == null){
89
- self::$reverseLookupCache[$IP] = '';
90
- } else {
91
- self::$reverseLookupCache[$IP] = $host[0]['target'];
92
- }
93
- }
94
- return self::$reverseLookupCache[$IP];
95
- }
96
public static function editUserLink($userID){
97
return get_admin_url() . 'user-edit.php?user_id=' . $userID;
98
}
@@ -196,6 +187,153 @@ class wfUtils {
196
}
197
return false;
198
}
199
}
200
201
1
<?php
2
class wfUtils {
3
+ private static $isWindows = false;
4
+ public static $scanLockFH = false;
5
public static function makeTimeAgo($secs, $noSeconds = false) {
6
if($secs < 1){
7
return "a moment";
67
return plugins_url() . '/wordfence/';
68
}
69
public static function getPluginBaseDir(){
70
+ return WP_CONTENT_DIR . '/plugins/';
71
+ //return ABSPATH . 'wp-content/plugins/';
72
}
73
public static function getIP(){
74
$ip = 0;
83
public static function getRequestedURL(){
84
return ($_SERVER['HTTPS'] ? 'https' : 'http') . '://' . $_SERVER['SERVER_NAME'] . $_SERVER['REQUEST_URI'];
85
}
86
+
87
public static function editUserLink($userID){
88
return get_admin_url() . 'user-edit.php?user_id=' . $userID;
89
}
187
}
188
return false;
189
}
190
+ public static function isWindows(){
191
+ if(! self::$isWindows){
192
+ if(preg_match('/^win/', PHP_OS)){
193
+ self::$isWindows = 'yes';
194
+ } else {
195
+ self::$isWindows = 'no';
196
+ }
197
+ }
198
+ return self::$isWindows == 'yes' ? true : false;
199
+ }
200
+ public static function getScanLock(){
201
+ if(self::isWindows()){
202
+ //Windows does not support non-blocking flock, so we use time.
203
+ $scanRunning = wfConfig::get('wf_scanRunning');
204
+ if($scanRunning && time() - $scanRunning < WORDFENCE_MAX_SCAN_TIME){
205
+ return false;
206
+ }
207
+ wfConfig::set('wf_scanRunning', time());
208
+ return true;
209
+ } else {
210
+ self::$scanLockFH = fopen(__FILE__, 'r');
211
+ if(flock(self::$scanLockFH, LOCK_EX | LOCK_NB)){
212
+ return true;
213
+ } else {
214
+ return false;
215
+ }
216
+ }
217
+ }
218
+ public static function clearScanLock(){
219
+ if(self::isWindows()){
220
+ wfConfig::set('wf_scanRunning', '');
221
+ } else {
222
+ if(self::$scanLockFH){
223
+ @fclose(self::$scanLockFH);
224
+ self::$scanLockFH = false;
225
+ }
226
+ }
227
+
228
+ }
229
+ public static function isScanRunning(){
230
+ $scanRunning = true;
231
+ if(self::getScanLock()){
232
+ $scanRunning = false;
233
+ }
234
+ self::clearScanLock();
235
+ return $scanRunning;
236
+ }
237
+ public static function getIPGeo($IP){ //Works with int or dotted
238
+
239
+ $locs = self::getIPsGeo(array($IP));
240
+ if(isset($locs[$IP])){
241
+ return $locs[$IP];
242
+ } else {
243
+ return false;
244
+ }
245
+ }
246
+ public static function getIPsGeo($IPs){ //works with int or dotted. Outputs same format it receives.
247
+ $IPs = array_unique($IPs);
248
+ $isInt = false;
249
+ if(strpos($IPs[0], '.') === false){
250
+ $isInt = true;
251
+ }
252
+ $toResolve = array();
253
+ $db = new wfDB();
254
+ global $wp_version;
255
+ global $wpdb;
256
+ $locsTable = $wpdb->base_prefix . 'wfLocs';
257
+ $IPLocs = array();
258
+ foreach($IPs as $IP){
259
+ $r1 = $db->query("select IP, ctime, failed, city, region, countryName, countryCode, lat, lon, unix_timestamp() - ctime as age from " . $locsTable . " where IP=%s", ($isInt ? $IP : self::inet_aton($IP)) );
260
+ if($r1){
261
+ if($row = mysql_fetch_assoc($r1)){
262
+ if($row['age'] > WORDFENCE_MAX_IPLOC_AGE){
263
+ $db->query("delete from " . $locsTable . " where IP=%s", $row['IP']);
264
+ } else {
265
+ if($row['failed'] == 1){
266
+ $IPLocs[$IP] = false;
267
+ } else {
268
+ if(! $isInt){
269
+ $row['IP'] = self::inet_ntoa($row['IP']);
270
+ }
271
+ $IPLocs[$IP] = $row;
272
+ }
273
+ }
274
+ }
275
+ }
276
+ if(! isset($IPLocs[$IP])){
277
+ $toResolve[] = $IP;
278
+ }
279
+ }
280
+ if(sizeof($toResolve) > 0){
281
+ $api = new wfAPI(wfConfig::get('apiKey'), $wp_version);
282
+ $freshIPs = $api->call('resolve_ips', array(), array(
283
+ 'ips' => implode(',', $toResolve)
284
+ ));
285
+ if(is_array($freshIPs)){
286
+ foreach($freshIPs as $IP => $value){
287
+ if($value == 'failed'){
288
+ $db->query("insert IGNORE into " . $locsTable . " (IP, ctime, failed) values (%s, unix_timestamp(), 1)", ($isInt ? $IP : self::inet_aton($IP)) );
289
+ $IPLocs[$IP] = false;
290
+ } else {
291
+ $db->query("insert IGNORE into " . $locsTable . " (IP, ctime, failed, city, region, countryName, countryCode, lat, lon) values (%s, unix_timestamp(), 0, '%s', '%s', '%s', '%s', %s, %s)",
292
+ ($isInt ? $IP : self::inet_aton($IP)),
293
+ $value[3], //city
294
+ $value[2], //region
295
+ $value[1], //countryName
296
+ $value[0],//countryCode
297
+ $value[4],//lat
298
+ $value[5]//lon
299
+ );
300
+ $IPLocs[$IP] = array(
301
+ 'IP' => $IP,
302
+ 'city' => $value[3],
303
+ 'region' => $value[2],
304
+ 'countryName' => $value[1],
305
+ 'countryCode' => $value[0],
306
+ 'lat' => $value[4],
307
+ 'lon' => $value[5]
308
+ );
309
+ }
310
+ }
311
+ }
312
+ }
313
+ return $IPLocs;
314
+ }
315
+ public function reverseLookup($IP){
316
+ $db = new wfDB();
317
+ global $wpdb;
318
+ $reverseTable = $wpdb->base_prefix . 'wfReverseCache';
319
+ $IPn = wfUtils::inet_aton($IP);
320
+ $host = $db->querySingle("select host from " . $reverseTable . " where IP=%s and unix_timestamp() - lastUpdate < %d", $IPn, WORDFENCE_REVERSE_LOOKUP_CACHE_TIME);
321
+ if(! $host){
322
+ $ptr = implode(".", array_reverse(explode(".",$IP))) . ".in-addr.arpa";
323
+ $host = dns_get_record($ptr, DNS_PTR);
324
+ if($host == null){
325
+ $host = 'NONE';
326
+ } else {
327
+ $host = $host[0]['target'];
328
+ }
329
+ $db->query("insert into " . $reverseTable . " (IP, host, lastUpdate) values (%s, '%s', unix_timestamp()) ON DUPLICATE KEY UPDATE host='%s', lastUpdate=unix_timestamp()", $IPn, $host, $host);
330
+ }
331
+ if($host == 'NONE'){
332
+ return '';
333
+ } else {
334
+ return $host;
335
+ }
336
+ }
337
}
338
339
lib/wordfenceClass.php CHANGED
@@ -165,8 +165,9 @@ class wordfence {
165
wp_schedule_event(time(), 'hourly', 'wordfence_daily_cron');
166
}
167
$db = new wfDB();
168
//Upgrading from 1.5.6 or earlier needs:
169
- $db->createKeyIfNotExists($prefix . 'wfStatus', 'level', 'k2');
170
if(wfConfig::get('isPaid') == 'free'){
171
wfConfig::set('isPaid', '');
172
}
@@ -180,6 +181,10 @@ class wordfence {
180
@chmod(dirname(__FILE__) . '/../wfscan.php', 0755);
181
@chmod(dirname(__FILE__) . '/../visitor.php', 0755);
182
183
//Must be the final line
184
update_option('wordfence_version', WORDFENCE_VERSION);
185
}
@@ -269,7 +274,7 @@ class wordfence {
269
$user = get_user_by('email', $_POST['user_login']);
270
if($user){
271
if(wfConfig::get('alertOn_lostPasswdForm')){
272
- wordfence::alert("Password recovery attempted", "Someone tried to recover the password for user with email address: $email\nTheir IP address was: " . wfUtils::getIP() . "\nTheir hostname was: " . self::getLog()->reverseLookup(wfUtils::getIP()));
273
}
274
}
275
if(wfConfig::get('loginSecurityEnabled')){
@@ -289,7 +294,7 @@ class wordfence {
289
}
290
public static function lockOutIP($IP, $reason){
291
if(wfConfig::get('alertOn_loginLockout')){
292
- wordfence::alert("User locked out from signing in", "A user with IP address $IP has been locked out from the signing in or using the password recovery form for the following reason: $reason");
293
}
294
self::getLog()->lockOutIP(wfUtils::getIP(), $reason);
295
}
@@ -297,7 +302,7 @@ class wordfence {
297
return self::getLog()->isIPLockedOut($IP);
298
}
299
public static function veryFirstAction(){
300
- $wfFunc = $_GET['_wfsf'];
301
if($wfFunc == 'unlockEmail'){
302
$email = trim($_POST['email']);
303
global $wpdb;
@@ -381,11 +386,11 @@ class wordfence {
381
self::getLog()->logLogin('loginOK', 0, $username);
382
if(user_can($userID, 'update_core')){
383
if(wfConfig::get('alertOn_adminLogin')){
384
- wordfence::alert("Admin Login", "A user with username \"$username\" who has administrator access signed in to your WordPress site.");
385
}
386
} else {
387
if(wfConfig::get('alertOn_nonAdminLogin')){
388
- wordfence::alert("User login", "A non-admin user with username \"$username\" signed in to your WordPress site.");
389
}
390
}
391
}
@@ -445,26 +450,25 @@ class wordfence {
445
}
446
public static function getWPFileContent($file, $cType, $cName, $cVersion){
447
if($cType == 'plugin'){
448
- $file = realpath(ABSPATH . $file);
449
- $file = substr($file, strlen(realpath(dirname(__FILE__) . '/../../')));
450
- $file = preg_replace('/^\/[^\/]+\//', '', $file);
451
} else if($cType == 'theme'){
452
- $themeDir = substr(WP_CONTENT_DIR, strlen(ABSPATH)) . get_theme_roots();
453
- $file = preg_replace('#' . $themeDir . '/[^/]+/#', '', $file);
454
} else if($cType == 'core'){
455
456
} else {
457
return array('errorMsg' => "An invalid type was specified to get file.");
458
}
459
-
460
- $transKey = 'wf_wpFileContent_' . $file . '_' . $cType . '_' . $cName . '_' . $cVersion;
461
- $transKey = preg_replace('/[^a-zA-Z0-9\_]+/', '_', $transKey);
462
- $content = get_site_transient($transKey);
463
- if($content){
464
- return array('fileContent' => $content);
465
- }
466
$api = new wfAPI(wfConfig::get('apiKey'), wfUtils::getWPVersion());
467
- $dat = $api->call('get_wp_file_content', array(
468
'file' => $file,
469
'cType' => $cType,
470
'cName' => $cName,
@@ -473,9 +477,8 @@ class wordfence {
473
if($api->errorMsg){
474
return array('errorMsg' => $api->errorMsg);
475
}
476
- if($dat['contents']){
477
- set_site_transient($transKey, $dat['contents'], WORDFENCE_TRANSIENTS_TIMEOUT);
478
- return array('fileContent' => $dat['contents']);
479
} else {
480
return array('errorMsg' => "We could not fetch a core WordPress file from the Wordfence API.");
481
}
@@ -683,7 +686,7 @@ class wordfence {
683
$ips = explode(',', $_POST['ips']);
684
$res = array();
685
foreach($ips as $ip){
686
- $res[$ip] = self::getLog()->reverseLookup($ip);
687
}
688
return array('ok' => 1, 'ips' => $res);
689
}
@@ -795,32 +798,37 @@ class wordfence {
795
$wfIssues = new wfIssues();
796
$issue = $wfIssues->getIssueByID($issueID);
797
if(! $issue){
798
- return array('errorMsg' => "We could not find that issue in our database.");
799
}
800
$dat = $issue['data'];
801
$result = self::getWPFileContent($dat['file'], $dat['cType'], $dat['cName'], $dat['cVersion']);
802
$file = $dat['file'];
803
- if($result['errorMsg']){
804
return $result;
805
} else if(! $result['fileContent']){
806
- return array('errorMsg' => "We could not get the original file to do a repair.");
807
}
808
809
if(preg_match('/\.\./', $file)){
810
- return array('errorMsg' => "An invalid file was specified for repair.");
811
}
812
$localFile = ABSPATH . '/' . preg_replace('/^[\.\/]+/', '', $file);
813
$fh = fopen($localFile, 'w');
814
if(! $fh){
815
$err = error_get_last();
816
- return array('errorMsg' => "We could not write to that file. The error was: " . $err['message']);
817
}
818
flock($fh, LOCK_EX);
819
$bytes = fwrite($fh, $result['fileContent']);
820
flock($fh, LOCK_UN);
821
fclose($fh);
822
if($bytes < 1){
823
- return array('errorMsg' => "We could not write to that file. ($bytes bytes written) You may not have permission to modify files on your WordPress server.");
824
}
825
$wfIssues->updateIssue($issueID, 'delete');
826
return array(
@@ -849,7 +857,7 @@ class wordfence {
849
}
850
if($result['ok'] && isset($result['isPaid'])){
851
wfConfig::set('isPaid', $result['isPaid']);
852
- $err = self::startScan();
853
if($err){
854
return array('errorMsg' => $err);
855
} else {
@@ -861,7 +869,7 @@ class wordfence {
861
}
862
public static function ajax_scan_callback(){
863
self::status(4, 'info', "Ajax request received to start scan.");
864
- $err = self::startScan();
865
if($err){
866
return array('errorMsg' => $err);
867
} else {
@@ -869,84 +877,7 @@ class wordfence {
869
}
870
}
871
public static function startScan(){
872
- self::status(4, 'info', "Entering start scan routine");
873
- $cron_url = plugins_url('wordfence/wfscan.php');
874
- self::status(4, 'info', "Cron URL is: " . $cron_url);
875
- $cronKey = wfUtils::bigRandomHex();
876
- self::status(4, 'info', "cronKey is: " . $cronKey);
877
- wfConfig::set('currentCronKey', time() . ',' . $cronKey);
878
- self::status(4, 'info', "cronKey is set");
879
- $result = wp_remote_post( $cron_url, array(
880
- 'timeout' => 0.5,
881
- 'blocking' => true,
882
- 'sslverify' => false,
883
- 'headers' => array(
884
- 'x-wordfence-cronkey' => $cronKey
885
- )
886
- ) );
887
- $procResp = self::processResponse($result);
888
- if($procResp){ return $procResp; }
889
- //If the currentCronKey was eaten, then cron executed so return
890
- wfConfig::clearCache(); if(! wfConfig::get('currentCronKey')){
891
- self::status(4, 'info', "cronkey is empty so cron executed. Returning.");
892
- return false;
893
- }
894
-
895
- //This second request is for hosts that don't know their own name. i.e. they don't have example.com in their hosts file or DNS pointing to their own IP address or loopback address. So we throw a hail mary to loopback.
896
- self::status(4, 'info', "cronkey is still set so sleeping for 0.2 seconds and checking again before trying another approach");
897
- usleep(200000);
898
- wfConfig::clearCache();
899
- if(wfConfig::get('currentCronKey')){ //cron key is still set, so cron hasn't executed yet. Maybe the request didn't go through
900
- self::status(4, 'info', "cronkey is still set so about to manually set host header and try again");
901
- $cron_url = preg_replace('/^(https?):\/\/[^\/]+/', '$1://127.0.0.1', $cron_url);
902
- self::status(4, 'info', "cron url is: $cron_url");
903
- $siteURL = site_url();
904
- self::status(4, 'info', "siteURL is: $siteURL");
905
- if(preg_match('/^https?:\/\/([^\/]+)/i', site_url(), $matches)){
906
- $host = $matches[1];
907
- self::status(4, 'info', "Extracted host $host from siteURL and trying remote post with manual host header set.");
908
- $result = wp_remote_post( $cron_url, array(
909
- 'timeout' => 0.5,
910
- 'blocking' => true,
911
- 'sslverify' => false,
912
- 'headers' => array(
913
- 'x-wordfence-cronkey' => $cronKey,
914
- 'Host' => $host
915
- )
916
- ) );
917
- $procResp = self::processResponse($result);
918
- if($procResp){ return $procResp; }
919
- }
920
- }
921
- return false;
922
- }
923
- public function processResponse($result){
924
- if((! is_wp_error($result)) && is_array($result) && empty($result['body']) === false){
925
- if(strpos($result['body'], 'WFSOURCEVISIBLE') !== false){
926
- self::status(4, 'info', "wfscan.php source is visible.");
927
- $msg = "Wordfence can't run because the source code of your WordPress plugin files is visible from the Internet. This is a serious security risk which you need to fix. Please look for .htaccess files in your WordPress root directory and your wp-content/ and wp-content/plugins/ directories that may contain malicious code designed to reveal your site source code to a hacker.";
928
- $htfiles = array();
929
- if(file_exists(ABSPATH . 'wp-content/.htaccess')){
930
- array_push($htfiles, '<a href="' . wfUtils::getSiteBaseURL() . '?_wfsf=view&nonce=' . wp_create_nonce('wp-ajax') . '&file=wp-content/.htaccess" target="_blank">wp-content/.htaccess</a>');
931
- }
932
- if(file_exists(ABSPATH . 'wp-content/plugins/.htaccess')){
933
- array_push($htfiles, '<a href="' . wfUtils::getSiteBaseURL() . '?_wfsf=view&nonce=' . wp_create_nonce('wp-ajax') . '&file=wp-content/plugins/.htaccess" target="_blank">wp-content/plugins/.htaccess</a>');
934
- }
935
- if(sizeof($htfiles) > 0){
936
- $msg .= "<br /><br />Click to view the .htaccess files below that may be the cause of this problem:<br />" . implode('<br />', $htfiles);
937
- }
938
- return $msg;
939
-
940
- } else if(strpos($result['body'], '{') !== false && strpos($result['body'], 'errorMsg') !== false){
941
- self::status(4, 'info', "Got response from cron containing json");
942
- $resp = json_decode($result['body'], true);
943
- if(empty($resp['errorMsg']) === false){
944
- self::status(4, 'info', "Got an error message from cron: " . $resp['errorMsg']);
945
- return $resp['errorMsg'];
946
- }
947
- }
948
- }
949
- return false;
950
}
951
public static function templateRedir(){
952
$wfFunc = get_query_var('_wfsf');
@@ -989,7 +920,7 @@ class wordfence {
989
self::wfFunc_testmem();
990
} else if($wfFunc == 'testtime'){
991
self::wfFunc_testtime();
992
- }
993
exit(0);
994
}
995
public static function memtest_error_handler($errno, $errstr, $errfile, $errline){
@@ -1063,7 +994,7 @@ class wordfence {
1063
}
1064
public static function wfFunc_IPTraf(){
1065
$IP = $_GET['IP'];
1066
- $reverseLookup = self::getLog()->reverseLookup($IP);
1067
if(! preg_match('/^\d+\.\d+\.\d+\.\d+#x2F;', $IP)){
1068
echo "An invalid IP address was specified.";
1069
exit(0);
@@ -1125,21 +1056,19 @@ class wordfence {
1125
}
1126
1127
$localFile = realpath(ABSPATH . '/' . preg_replace('/^[\.\/]+/', '', $_GET['file']));
1128
- if(strpos($localFile, ABSPATH) !== 0){
1129
- echo "An invalid file was requested for comparison.";
1130
- exit(0);
1131
- }
1132
- $diffOptions = array();
1133
$localContents = file_get_contents($localFile);
1134
-
1135
- $diff = new Diff(
1136
- //Treat DOS and Unix files the same
1137
- preg_split("/(?:\r\n|\n)/", $result['fileContent']),
1138
- preg_split("/(?:\r\n|\n)/", $localContents),
1139
- array()
1140
- );
1141
- $renderer = new Diff_Renderer_Html_SideBySide;
1142
- $diffResult = $diff->Render($renderer);
1143
require 'diffResult.php';
1144
exit(0);
1145
}
@@ -1154,7 +1083,7 @@ class wordfence {
1154
} else {
1155
self::$newVisit = true;
1156
}
1157
- setcookie($cookieName, uniqid(), time() + 1800, '/');
1158
}
1159
public static function admin_init(){
1160
if(! wfUtils::isAdmin()){ return; }
@@ -1162,7 +1091,7 @@ class wordfence {
1162
add_action('wp_ajax_wordfence_' . $func, 'wordfence::ajaxReceiver');
1163
}
1164
1165
- if(preg_match('/^Wordfence/', $_GET['page'])){
1166
1167
wp_enqueue_style('wordfence-main-style', WP_PLUGIN_URL . '/wordfence/css/main.css', '', WORDFENCE_VERSION);
1168
wp_enqueue_style('wordfence-colorbox-style', WP_PLUGIN_URL . '/wordfence/css/colorbox.css', '', WORDFENCE_VERSION);
@@ -1203,7 +1132,7 @@ class wordfence {
1203
}
1204
}
1205
add_submenu_page("Wordfence", "Scan", "Scan", "activate_plugins", "Wordfence", 'wordfence::menu_scan');
1206
- add_menu_page('Wordfence', 'Wordfence', 'activate_plugins', 'Wordfence', 'wordfence::menu_scan', WP_PLUGIN_URL . '/wordfence/images/wordfence-logo-16x16.png', 'div');
1207
if(wfConfig::get('liveTrafficEnabled')){
1208
add_submenu_page("Wordfence", "Live Traffic", "Live Traffic", "activate_plugins", "WordfenceActivity", 'wordfence::menu_activity');
1209
}
@@ -1273,18 +1202,36 @@ class wordfence {
1273
return admin_url('admin.php?page=WordfenceSecOpt', 'http');
1274
}
1275
1276
- public static function alert($subject, $alertMsg){
1277
$content = wfUtils::tmpl('email_genericAlert.php', array(
1278
'subject' => $subject,
1279
'blogName' => get_bloginfo('name', 'raw'),
1280
'alertMsg' => $alertMsg,
1281
'date' => date('l jS \of F Y \a\t h:i:s A'),
1282
'myHomeURL' => self::getMyHomeURL(),
1283
'myOptionsURL' => self::getMyOptionsURL()
1284
));
1285
$emails = wfConfig::getAlertEmails();
1286
if(sizeof($emails) < 1){ return; }
1287
- $subject = "[Wordfence Alert] " . $subject;
1288
wp_mail(implode(',', $emails), $subject, $content);
1289
}
1290
public static function scheduleNextScan($force = false){
@@ -1316,24 +1263,34 @@ class wordfence {
1316
}
1317
return self::$wfLog;
1318
}
1319
public static function statusStart($msg){
1320
- self::$statusStartMsgs[] = $msg;
1321
self::status(10, 'info', 'SUM_START:' . $msg);
1322
- return sizeof(self::$statusStartMsgs) - 1;
1323
}
1324
public static function statusEnd($idx, $haveIssues){
1325
if($haveIssues){
1326
- self::status(10, 'info', 'SUM_ENDBAD:' . self::$statusStartMsgs[$idx]);
1327
} else {
1328
- self::status(10, 'info', 'SUM_ENDOK:' . self::$statusStartMsgs[$idx]);
1329
}
1330
- self::$statusStartMsgs[$idx] = '';
1331
}
1332
public static function statusEndErr(){
1333
- for($i = 0; $i < sizeof(self::$statusStartMsgs); $i++){
1334
- if(empty(self::$statusStartMsgs[$i]) === false){
1335
- self::status(10, 'info', 'SUM_ENDERR:' . self::$statusStartMsgs[$i]);
1336
- self::$statusStartMsgs[$i] = '';
1337
}
1338
}
1339
}
165
wp_schedule_event(time(), 'hourly', 'wordfence_daily_cron');
166
}
167
$db = new wfDB();
168
+
169
//Upgrading from 1.5.6 or earlier needs:
170
+ $db->createKeyIfNotExists('wfStatus', 'level', 'k2');
171
if(wfConfig::get('isPaid') == 'free'){
172
wfConfig::set('isPaid', '');
173
}
181
@chmod(dirname(__FILE__) . '/../wfscan.php', 0755);
182
@chmod(dirname(__FILE__) . '/../visitor.php', 0755);
183
184
+ global $wpdb;
185
+ $prefix = $wpdb->base_prefix;
186
+ $db->query("alter table $prefix"."wfConfig modify column val longblob");
187
+
188
//Must be the final line
189
update_option('wordfence_version', WORDFENCE_VERSION);
190
}
274
$user = get_user_by('email', $_POST['user_login']);
275
if($user){
276
if(wfConfig::get('alertOn_lostPasswdForm')){
277
+ wordfence::alert("Password recovery attempted", "Someone tried to recover the password for user with email address: $email", wfUtils::getIP());
278
}
279
}
280
if(wfConfig::get('loginSecurityEnabled')){
294
}
295
public static function lockOutIP($IP, $reason){
296
if(wfConfig::get('alertOn_loginLockout')){
297
+ wordfence::alert("User locked out from signing in", "A user with IP address $IP has been locked out from the signing in or using the password recovery form for the following reason: $reason", $IP);
298
}
299
self::getLog()->lockOutIP(wfUtils::getIP(), $reason);
300
}
302
return self::getLog()->isIPLockedOut($IP);
303
}
304
public static function veryFirstAction(){
305
+ $wfFunc = @$_GET['_wfsf'];
306
if($wfFunc == 'unlockEmail'){
307
$email = trim($_POST['email']);
308
global $wpdb;
386
self::getLog()->logLogin('loginOK', 0, $username);
387
if(user_can($userID, 'update_core')){
388
if(wfConfig::get('alertOn_adminLogin')){
389
+ wordfence::alert("Admin Login", "A user with username \"$username\" who has administrator access signed in to your WordPress site.", wfUtils::getIP());
390
}
391
} else {
392
if(wfConfig::get('alertOn_nonAdminLogin')){
393
+ wordfence::alert("User login", "A non-admin user with username \"$username\" signed in to your WordPress site.", wfUtils::getIP());
394
}
395
}
396
}
450
}
451
public static function getWPFileContent($file, $cType, $cName, $cVersion){
452
if($cType == 'plugin'){
453
+ if(preg_match('#^/?wp-content/plugins/[^/]+/#', $file)){
454
+ $file = preg_replace('#^/?wp-content/plugins/[^/]+/#', '', $file);
455
+ } else {
456
+ //If user is using non-standard wp-content dir, then use /plugins/ in pattern to figure out what to strip off
457
+ $file = preg_replace('#^.*[^/]+/plugins/[^/]+/#', '', $file);
458
+ }
459
} else if($cType == 'theme'){
460
+ if(preg_match('#/?wp-content/themes/[^/]+/#', $file)){
461
+ $file = preg_replace('#/?wp-content/themes/[^/]+/#', '', $file);
462
+ } else {
463
+ $file = preg_replace('#^.*[^/]+/themes/[^/]+/#', '', $file);
464
+ }
465
} else if($cType == 'core'){
466
467
} else {
468
return array('errorMsg' => "An invalid type was specified to get file.");
469
}
470
$api = new wfAPI(wfConfig::get('apiKey'), wfUtils::getWPVersion());
471
+ $contResult = $api->binCall('get_wp_file_content', array(
472
'file' => $file,
473
'cType' => $cType,
474
'cName' => $cName,
477
if($api->errorMsg){
478
return array('errorMsg' => $api->errorMsg);
479
}
480
+ if($contResult['data']){
481
+ return array('fileContent' => $contResult['data']);
482
} else {
483
return array('errorMsg' => "We could not fetch a core WordPress file from the Wordfence API.");
484
}
686
$ips = explode(',', $_POST['ips']);
687
$res = array();
688
foreach($ips as $ip){
689
+ $res[$ip] = wfUtils::reverseLookup($ip);
690
}
691
return array('ok' => 1, 'ips' => $res);
692
}
798
$wfIssues = new wfIssues();
799
$issue = $wfIssues->getIssueByID($issueID);
800
if(! $issue){
801
+ return array('cerrorMsg' => "We could not find that issue in our database.");
802
}
803
$dat = $issue['data'];
804
$result = self::getWPFileContent($dat['file'], $dat['cType'], $dat['cName'], $dat['cVersion']);
805
$file = $dat['file'];
806
+ if($result['cerrorMsg']){
807
return $result;
808
} else if(! $result['fileContent']){
809
+ return array('cerrorMsg' => "We could not get the original file to do a repair.");
810
}
811
812
if(preg_match('/\.\./', $file)){
813
+ return array('cerrorMsg' => "An invalid file was specified for repair.");
814
}
815
$localFile = ABSPATH . '/' . preg_replace('/^[\.\/]+/', '', $file);
816
$fh = fopen($localFile, 'w');
817
if(! $fh){
818
$err = error_get_last();
819
+ if(preg_match('/Permission denied/i', $err['message'])){
820
+ $errMsg = "You don't have permission to repair that file. You need to either fix the file manually using FTP or change the file permissions and ownership so that your web server has write access to repair the file.";
821
+ } else {
822
+ $errMsg = "We could not write to that file. The error was: " . $err['message'];
823
+ }
824
+ return array('cerrorMsg' => $errMsg);
825
}
826
flock($fh, LOCK_EX);
827
$bytes = fwrite($fh, $result['fileContent']);
828
flock($fh, LOCK_UN);
829
fclose($fh);
830
if($bytes < 1){
831
+ return array('cerrorMsg' => "We could not write to that file. ($bytes bytes written) You may not have permission to modify files on your WordPress server.");
832
}
833
$wfIssues->updateIssue($issueID, 'delete');
834
return array(
857
}
858
if($result['ok'] && isset($result['isPaid'])){
859
wfConfig::set('isPaid', $result['isPaid']);
860
+ $err = wfScanEngine::startScan();
861
if($err){
862
return array('errorMsg' => $err);
863
} else {
869
}
870
public static function ajax_scan_callback(){
871
self::status(4, 'info', "Ajax request received to start scan.");
872
+ $err = wfScanEngine::startScan();
873
if($err){
874
return array('errorMsg' => $err);
875
} else {
877
}
878
}
879
public static function startScan(){
880
+ wfScanEngine::startScan();