Wordfence Security – Firewall & Malware Scan - Version 6.3.14

Version Description

  • Improvement: Introduced smart scan distribution. Scan times are now distributed intelligently across servers to provide consistent server performance.
  • Improvement: Introduced light-weight scan that runs frequently to perform checks that do not use any server resources.
  • Improvement: If unable to successfully look up the status of an IP claiming to be Googlebot, the hit is now allowed.
  • Improvement: Scan issue results for abandoned plugins and unpatched vulnerabilities include more info.
  • Fix: Suppressed PHP notice with time formatting when a microtimestamp is passed.
  • Fix: Improved binary data to HTML entity conversion to avoid wpdb stripping out-of-range UTF-8 sequences.
  • Fix: Added better detection to SSL status, particularly for IIS.
  • Fix: Fixed PHP notice in the diff renderer.
  • Fix: Fixed typo in lockout alert.
Download this release

Release Info

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

Code changes from version 6.3.12 to 6.3.14

lib/Diff/Renderer/Html/Array.php CHANGED
@@ -190,7 +190,8 @@ class Diff_Renderer_Html_Array extends Diff_Renderer_Abstract
190
  */
191
  private function fixSpacesCallback($matches)
192
  {
193
- return $this->fixSpaces($matches[1]);
 
194
  }
195
 
196
  /**
190
  */
191
  private function fixSpacesCallback($matches)
192
  {
193
+ $spaces = (isset($matches[1]) ? $matches[1] : '');
194
+ return $this->fixSpaces($spaces);
195
  }
196
 
197
  /**
lib/email_newIssues.php CHANGED
@@ -25,18 +25,29 @@
25
  <?php
26
  if ((isset($i['tmplData']['wpRemoved']) && $i['tmplData']['wpRemoved']) || (isset($i['tmplData']['abandoned']) && $i['tmplData']['abandoned'])) {
27
  if (isset($i['tmplData']['vulnerable']) && $i['tmplData']['vulnerable']) {
28
- echo '<p><strong>Plugin contains an unpatched security vulnerability.</strong></p>';
 
 
 
 
29
  }
30
  }
31
  else if (isset($i['tmplData']['wpURL'])) {
32
  echo '<p>';
33
  if (isset($i['tmplData']['vulnerable']) && $i['tmplData']['vulnerable']) {
34
  echo '<strong>Update includes security-related fixes.</strong> ';
 
 
 
35
  }
36
  echo $i['tmplData']['wpURL'] . '/#developers</p>';
37
  }
38
  else if (isset($i['tmplData']['vulnerable']) && $i['tmplData']['vulnerable']) {
39
- echo '<p><strong>Update includes security-related fixes.</strong></p>';
 
 
 
 
40
  }
41
  ?>
42
  <?php if (!empty($i['tmplData']['badURL'])): ?>
25
  <?php
26
  if ((isset($i['tmplData']['wpRemoved']) && $i['tmplData']['wpRemoved']) || (isset($i['tmplData']['abandoned']) && $i['tmplData']['abandoned'])) {
27
  if (isset($i['tmplData']['vulnerable']) && $i['tmplData']['vulnerable']) {
28
+ echo '<p><strong>Plugin contains an unpatched security vulnerability.</strong>';
29
+ if (isset($i['tmplData']['vulnerabilityLink'])) {
30
+ echo ' <a href="' . $i['tmplData']['vulnerabilityLink'] . '" target="_blank" rel="nofollow noreferer noopener">Vulnerability Information</a>';
31
+ }
32
+ echo '</p>';
33
  }
34
  }
35
  else if (isset($i['tmplData']['wpURL'])) {
36
  echo '<p>';
37
  if (isset($i['tmplData']['vulnerable']) && $i['tmplData']['vulnerable']) {
38
  echo '<strong>Update includes security-related fixes.</strong> ';
39
+ if (isset($i['tmplData']['vulnerabilityLink'])) {
40
+ echo '<a href="' . $i['tmplData']['vulnerabilityLink'] . '" target="_blank" rel="nofollow noreferer noopener">Vulnerability Information</a> ';
41
+ }
42
  }
43
  echo $i['tmplData']['wpURL'] . '/#developers</p>';
44
  }
45
  else if (isset($i['tmplData']['vulnerable']) && $i['tmplData']['vulnerable']) {
46
+ echo '<p><strong>Update includes security-related fixes.</strong>';
47
+ if (isset($i['tmplData']['vulnerabilityLink'])) {
48
+ echo ' <a href="' . $i['tmplData']['vulnerabilityLink'] . '" target="_blank" rel="nofollow noreferer noopener">Vulnerability Information</a>';
49
+ }
50
+ echo '</p>';
51
  }
52
  ?>
53
  <?php if (!empty($i['tmplData']['badURL'])): ?>
lib/menu_scan.php CHANGED
@@ -271,6 +271,7 @@ $sigUpdateTime = wfConfig::get('signatureUpdateTime');
271
  <tr><th>New <span class="wf-hidden-xs">Theme </span>Version:</th><td>${data.newVersion}</td></tr>
272
  <tr><th><span class="wf-hidden-xs">Theme </span>URL:</th><td><a href="${data.URL}" target="_blank" rel="noopener noreferrer"><span class="wf-hidden-xs wf-split-word">${data.URL}</span><span class="wf-visible-xs">View</span></a></td></tr>
273
  <tr><th>Severity:</th><td>{{if severity == '1'}}Critical{{else}}Warning{{/if}}</td></tr>
 
274
  <tr><th>Status</th><td>
275
  {{if status == 'new' }}New{{/if}}
276
  {{if status == 'ignoreP' || status == 'ignoreC' }}Ignored{{/if}}
@@ -312,6 +313,7 @@ $sigUpdateTime = wfConfig::get('signatureUpdateTime');
312
  <tr><th>Current <span class="wf-hidden-xs">Plugin </span>Version:</th><td>${data.Version}</td></tr>
313
  <tr><th>New <span class="wf-hidden-xs">Plugin </span>Version:</th><td>${data.newVersion}</td></tr>
314
  <tr><th>Severity:</th><td>{{if severity == '1'}}Critical{{else}}Warning{{/if}}</td></tr>
 
315
  <tr><th>Status</th><td>
316
  {{if status == 'new' }}New{{/if}}
317
  {{if status == 'ignoreP' || status == 'ignoreC' }}Ignored{{/if}}
@@ -351,6 +353,7 @@ $sigUpdateTime = wfConfig::get('signatureUpdateTime');
351
  {{if data.PluginURI}}<tr><th><span class="wf-hidden-xs">Plugin </span>Website:</th><td><a href="${data.PluginURI}" target="_blank" rel="noopener noreferrer"><span class="wf-hidden-xs wf-split-word">${data.PluginURI}</span><span class="wf-visible-xs">View</span></a></td></tr>{{/if}}
352
  <tr><th>Current <span class="wf-hidden-xs">Plugin </span>Version:</th><td>${data.Version}</td></tr>
353
  <tr><th>Severity:</th><td>{{if severity == '1'}}Critical{{else}}Warning{{/if}}</td></tr>
 
354
  <tr><th>Status</th><td>
355
  {{if status == 'new' }}New{{/if}}
356
  {{if status == 'ignoreP' || status == 'ignoreC' }}Ignored{{/if}}
@@ -390,6 +393,7 @@ $sigUpdateTime = wfConfig::get('signatureUpdateTime');
390
  <tr><th>Current <span class="wf-hidden-xs">Plugin </span>Version:</th><td>${data.version}</td></tr>
391
  <tr><th>Last Updated:</th><td>${data.dateUpdated}</td></tr>
392
  <tr><th>Severity:</th><td>{{if severity == '1'}}Critical{{else}}Warning{{/if}}</td></tr>
 
393
  <tr><th>Status</th><td>
394
  {{if status == 'new' }}New{{/if}}
395
  {{if status == 'ignoreP' || status == 'ignoreC' }}Ignored{{/if}}
@@ -429,6 +433,7 @@ $sigUpdateTime = wfConfig::get('signatureUpdateTime');
429
  {{if data.wpURL}}<tr><th>Repository<span class="wf-hidden-xs"> Link</span>:</th><td><a href="${data.wpURL}" target="_blank" rel="noopener noreferrer"><span class="wf-hidden-xs wf-split-word">${data.wpURL}</span><span class="wf-visible-xs">View</span></a></td></tr>{{/if}}
430
  <tr><th>Current <span class="wf-hidden-xs">Plugin </span>Version:</th><td>${data.Version}</td></tr>
431
  <tr><th>Severity:</th><td>{{if severity == '1'}}Critical{{else}}Warning{{/if}}</td></tr>
 
432
  <tr><th>Status</th><td>
433
  {{if status == 'new' }}New{{/if}}
434
  {{if status == 'ignoreP' || status == 'ignoreC' }}Ignored{{/if}}
@@ -465,6 +470,7 @@ $sigUpdateTime = wfConfig::get('signatureUpdateTime');
465
  <tr><th>Current <span class="wf-hidden-xs">WordPress </span>Version:</th><td>${data.currentVersion}</td></tr>
466
  <tr><th>New <span class="wf-hidden-xs">WordPress </span>Version:</th><td>${data.newVersion}</td></tr>
467
  <tr><th>Severity:</th><td>{{if severity == '1'}}Critical{{else}}Warning{{/if}}</td></tr>
 
468
  <tr><th>Status</th><td>
469
  {{if status == 'new' }}New{{/if}}
470
  {{if status == 'ignoreP' || status == 'ignoreC' }}Ignored{{/if}}
271
  <tr><th>New <span class="wf-hidden-xs">Theme </span>Version:</th><td>${data.newVersion}</td></tr>
272
  <tr><th><span class="wf-hidden-xs">Theme </span>URL:</th><td><a href="${data.URL}" target="_blank" rel="noopener noreferrer"><span class="wf-hidden-xs wf-split-word">${data.URL}</span><span class="wf-visible-xs">View</span></a></td></tr>
273
  <tr><th>Severity:</th><td>{{if severity == '1'}}Critical{{else}}Warning{{/if}}</td></tr>
274
+ {{if data.vulnerabilityLink}}<tr><th>Vulnerability <span class="wf-hidden-xs">Information</span>:</th><td><a href="${data.vulnerabilityLink}" target="_blank" rel="noopener noreferrer"><span class="wf-hidden-xs wf-split-word">${data.vulnerabilityLink}</span><span class="wf-visible-xs">View</span></a></td></tr>{{/if}}
275
  <tr><th>Status</th><td>
276
  {{if status == 'new' }}New{{/if}}
277
  {{if status == 'ignoreP' || status == 'ignoreC' }}Ignored{{/if}}
313
  <tr><th>Current <span class="wf-hidden-xs">Plugin </span>Version:</th><td>${data.Version}</td></tr>
314
  <tr><th>New <span class="wf-hidden-xs">Plugin </span>Version:</th><td>${data.newVersion}</td></tr>
315
  <tr><th>Severity:</th><td>{{if severity == '1'}}Critical{{else}}Warning{{/if}}</td></tr>
316
+ {{if data.vulnerabilityLink}}<tr><th>Vulnerability <span class="wf-hidden-xs">Information</span>:</th><td><a href="${data.vulnerabilityLink}" target="_blank" rel="noopener noreferrer"><span class="wf-hidden-xs wf-split-word">${data.vulnerabilityLink}</span><span class="wf-visible-xs">View</span></a></td></tr>{{/if}}
317
  <tr><th>Status</th><td>
318
  {{if status == 'new' }}New{{/if}}
319
  {{if status == 'ignoreP' || status == 'ignoreC' }}Ignored{{/if}}
353
  {{if data.PluginURI}}<tr><th><span class="wf-hidden-xs">Plugin </span>Website:</th><td><a href="${data.PluginURI}" target="_blank" rel="noopener noreferrer"><span class="wf-hidden-xs wf-split-word">${data.PluginURI}</span><span class="wf-visible-xs">View</span></a></td></tr>{{/if}}
354
  <tr><th>Current <span class="wf-hidden-xs">Plugin </span>Version:</th><td>${data.Version}</td></tr>
355
  <tr><th>Severity:</th><td>{{if severity == '1'}}Critical{{else}}Warning{{/if}}</td></tr>
356
+ {{if data.vulnerabilityLink}}<tr><th>Vulnerability <span class="wf-hidden-xs">Information</span>:</th><td><a href="${data.vulnerabilityLink}" target="_blank" rel="noopener noreferrer"><span class="wf-hidden-xs wf-split-word">${data.vulnerabilityLink}</span><span class="wf-visible-xs">View</span></a></td></tr>{{/if}}
357
  <tr><th>Status</th><td>
358
  {{if status == 'new' }}New{{/if}}
359
  {{if status == 'ignoreP' || status == 'ignoreC' }}Ignored{{/if}}
393
  <tr><th>Current <span class="wf-hidden-xs">Plugin </span>Version:</th><td>${data.version}</td></tr>
394
  <tr><th>Last Updated:</th><td>${data.dateUpdated}</td></tr>
395
  <tr><th>Severity:</th><td>{{if severity == '1'}}Critical{{else}}Warning{{/if}}</td></tr>
396
+ {{if data.vulnerabilityLink}}<tr><th>Vulnerability <span class="wf-hidden-xs">Information</span>:</th><td><a href="${data.vulnerabilityLink}" target="_blank" rel="noopener noreferrer"><span class="wf-hidden-xs wf-split-word">${data.vulnerabilityLink}</span><span class="wf-visible-xs">View</span></a></td></tr>{{/if}}
397
  <tr><th>Status</th><td>
398
  {{if status == 'new' }}New{{/if}}
399
  {{if status == 'ignoreP' || status == 'ignoreC' }}Ignored{{/if}}
433
  {{if data.wpURL}}<tr><th>Repository<span class="wf-hidden-xs"> Link</span>:</th><td><a href="${data.wpURL}" target="_blank" rel="noopener noreferrer"><span class="wf-hidden-xs wf-split-word">${data.wpURL}</span><span class="wf-visible-xs">View</span></a></td></tr>{{/if}}
434
  <tr><th>Current <span class="wf-hidden-xs">Plugin </span>Version:</th><td>${data.Version}</td></tr>
435
  <tr><th>Severity:</th><td>{{if severity == '1'}}Critical{{else}}Warning{{/if}}</td></tr>
436
+ {{if data.vulnerabilityLink}}<tr><th>Vulnerability <span class="wf-hidden-xs">Information</span>:</th><td><a href="${data.vulnerabilityLink}" target="_blank" rel="noopener noreferrer"><span class="wf-hidden-xs wf-split-word">${data.vulnerabilityLink}</span><span class="wf-visible-xs">View</span></a></td></tr>{{/if}}
437
  <tr><th>Status</th><td>
438
  {{if status == 'new' }}New{{/if}}
439
  {{if status == 'ignoreP' || status == 'ignoreC' }}Ignored{{/if}}
470
  <tr><th>Current <span class="wf-hidden-xs">WordPress </span>Version:</th><td>${data.currentVersion}</td></tr>
471
  <tr><th>New <span class="wf-hidden-xs">WordPress </span>Version:</th><td>${data.newVersion}</td></tr>
472
  <tr><th>Severity:</th><td>{{if severity == '1'}}Critical{{else}}Warning{{/if}}</td></tr>
473
+ {{if data.vulnerabilityLink}}<tr><th>Vulnerability <span class="wf-hidden-xs">Information</span>:</th><td><a href="${data.vulnerabilityLink}" target="_blank" rel="noopener noreferrer"><span class="wf-hidden-xs wf-split-word">${data.vulnerabilityLink}</span><span class="wf-visible-xs">View</span></a></td></tr>{{/if}}
474
  <tr><th>Status</th><td>
475
  {{if status == 'new' }}New{{/if}}
476
  {{if status == 'ignoreP' || status == 'ignoreC' }}Ignored{{/if}}
lib/menu_scan_schedule.php CHANGED
@@ -14,24 +14,23 @@
14
  <p class="center"><a class="wf-btn wf-btn-primary wf-btn-callout" href="https://www.wordfence.com/gnl1scanSched1/wordfence-signup/" target="_blank" rel="noopener noreferrer">Get Premium</a></p>
15
  </div>
16
  <?php } ?>
17
- <?php $schedMode = wfConfig::get('isPaid') ? wfConfig::get('schedMode', 'auto') : 'auto'; ?>
18
  <div class="wf-container-fluid">
19
  <div class="wf-row">
20
  <div class="wf-col-xs-12">
21
- <div class="wf-card wf-card-left<?php echo ($schedMode == 'auto' ? ' active' : ''); ?>" data-mode="auto">
22
  <div class="wf-card-inner">
23
  <div class="wf-card-content">
24
  <div class="wf-card-title">
25
  Let Wordfence automatically schedule scans (recommended)
26
  </div>
27
  <div class="wf-card-subtitle">
28
- <?php if ($schedMode == 'auto') : ?>
29
  <?php echo wordfence::getNextScanStartTime(); ?>
30
  <?php endif; ?>
31
  </div>
32
  </div>
33
  <div class="wf-card-action">
34
- <div class="wf-card-action-checkbox<?php echo ($schedMode == 'auto' ? ' checked' : ''); ?>"></div>
35
  </div>
36
  </div>
37
  </div>
@@ -39,20 +38,20 @@
39
  </div>
40
  <div class="wf-row">
41
  <div class="wf-col-xs-12">
42
- <div class="wf-card wf-card-left<?php echo ($schedMode == 'manual' ? ' active' : ''); ?><?php if (!wfConfig::get('isPaid')) { echo ' disabled'; } ?>" data-mode="manual">
43
  <div class="wf-card-inner">
44
  <div class="wf-card-content">
45
  <div class="wf-card-title">
46
  Manually schedule scans<?php if (!wfConfig::get('isPaid')) { echo ' (Premium Members Only)'; } ?>
47
  </div>
48
  <div class="wf-card-subtitle">
49
- <?php if ($schedMode == 'manual') : ?>
50
  <?php echo wordfence::getNextScanStartTime(); ?>
51
  <?php endif; ?>
52
  </div>
53
  </div>
54
  <div class="wf-card-action">
55
- <div class="wf-card-action-checkbox<?php echo ($schedMode == 'manual' ? ' checked' : ''); ?>"></div>
56
  </div>
57
  </div>
58
  <div class="wf-card-extra">
14
  <p class="center"><a class="wf-btn wf-btn-primary wf-btn-callout" href="https://www.wordfence.com/gnl1scanSched1/wordfence-signup/" target="_blank" rel="noopener noreferrer">Get Premium</a></p>
15
  </div>
16
  <?php } ?>
 
17
  <div class="wf-container-fluid">
18
  <div class="wf-row">
19
  <div class="wf-col-xs-12">
20
+ <div class="wf-card wf-card-left<?php echo (wfScan::isAutoScanSchedule() ? ' active' : ''); ?>" data-mode="auto">
21
  <div class="wf-card-inner">
22
  <div class="wf-card-content">
23
  <div class="wf-card-title">
24
  Let Wordfence automatically schedule scans (recommended)
25
  </div>
26
  <div class="wf-card-subtitle">
27
+ <?php if (wfScan::isAutoScanSchedule()) : ?>
28
  <?php echo wordfence::getNextScanStartTime(); ?>
29
  <?php endif; ?>
30
  </div>
31
  </div>
32
  <div class="wf-card-action">
33
+ <div class="wf-card-action-checkbox<?php echo (wfScan::isAutoScanSchedule() ? ' checked' : ''); ?>"></div>
34
  </div>
35
  </div>
36
  </div>
38
  </div>
39
  <div class="wf-row">
40
  <div class="wf-col-xs-12">
41
+ <div class="wf-card wf-card-left<?php echo (wfScan::isManualScanSchedule() ? ' active' : ''); ?><?php if (!wfConfig::get('isPaid')) { echo ' disabled'; } ?>" data-mode="manual">
42
  <div class="wf-card-inner">
43
  <div class="wf-card-content">
44
  <div class="wf-card-title">
45
  Manually schedule scans<?php if (!wfConfig::get('isPaid')) { echo ' (Premium Members Only)'; } ?>
46
  </div>
47
  <div class="wf-card-subtitle">
48
+ <?php if (wfScan::isManualScanSchedule()) : ?>
49
  <?php echo wordfence::getNextScanStartTime(); ?>
50
  <?php endif; ?>
51
  </div>
52
  </div>
53
  <div class="wf-card-action">
54
+ <div class="wf-card-action-checkbox<?php echo (wfScan::isManualScanSchedule() ? ' checked' : ''); ?>"></div>
55
  </div>
56
  </div>
57
  <div class="wf-card-extra">
lib/wfConfig.php CHANGED
@@ -130,7 +130,7 @@ class wfConfig {
130
  'howGetIPs_trusted_proxies' => '',
131
  )
132
  );
133
- public static $serializedOptions = array('lastAdminLogin', 'scanSched', 'emailedIssuesList', 'wf_summaryItems', 'adminUserList', 'twoFactorUsers', 'alertFreqTrack', 'wfStatusStartMsgs', 'vulnerabilities_plugin', 'vulnerabilities_theme', 'dashboardData', 'malwarePrefixes');
134
  public static function setDefaults() {
135
  foreach (self::$defaultConfig['checkboxes'] as $key => $config) {
136
  $val = $config['value'];
130
  'howGetIPs_trusted_proxies' => '',
131
  )
132
  );
133
+ public static $serializedOptions = array('lastAdminLogin', 'scanSched', 'emailedIssuesList', 'wf_summaryItems', 'adminUserList', 'twoFactorUsers', 'alertFreqTrack', 'wfStatusStartMsgs', 'vulnerabilities_plugin', 'vulnerabilities_theme', 'dashboardData', 'malwarePrefixes', 'noc1ScanSchedule', 'allScansScheduled');
134
  public static function setDefaults() {
135
  foreach (self::$defaultConfig['checkboxes'] as $key => $config) {
136
  $val = $config['value'];
lib/wfCrawl.php CHANGED
@@ -1,6 +1,10 @@
1
  <?php
2
  require_once('wfUtils.php');
3
  class wfCrawl {
 
 
 
 
4
  public static function isCrawler($UA){
5
  $browscap = new wfBrowscap();
6
  $b = $browscap->getBrowser($UA);
@@ -120,18 +124,27 @@ class wfCrawl {
120
  $verified[$ip] = true;
121
  return $verified[$ip];
122
  }
123
- if (self::verifyGooglebotViaNOC1($ip)) {
 
124
  $verified[$ip] = true;
125
  return $verified[$ip];
126
  }
 
 
 
 
 
 
127
  }
128
  $verified[$ip] = false;
129
  return $verified[$ip];
130
  }
131
 
132
  /**
 
 
133
  * @param string|null $ip
134
- * @return bool
135
  */
136
  public static function verifyGooglebotViaNOC1($ip = null) {
137
  global $wpdb;
@@ -150,9 +163,9 @@ class wfCrawl {
150
  $patternSig,
151
  WORDFENCE_CRAWLER_VERIFY_CACHE_TIME);
152
  if ($status === 'verified') {
153
- return true;
154
  } else if ($status === 'fakeBot') {
155
- return false;
156
  }
157
 
158
  $api = new wfAPI(wfConfig::get('apiKey'), wfUtils::getWPVersion());
@@ -166,17 +179,18 @@ class wfCrawl {
166
  values (%s, UNHEX(MD5('%s')), '%s', unix_timestamp())
167
  ON DUPLICATE KEY UPDATE status='%3\$s', lastUpdate=unix_timestamp()",
168
  $IPn, $patternSig, 'verified');
169
- return true;
170
  } else {
171
  $db->queryWrite("insert into $table (IP, patternSig, status, lastUpdate)
172
  values (%s, UNHEX(MD5('%s')), '%s', unix_timestamp())
173
  ON DUPLICATE KEY UPDATE status='%3\$s', lastUpdate=unix_timestamp()",
174
  $IPn, $patternSig, 'fakeBot');
 
175
  }
176
  } catch (Exception $e) {
177
  // Do nothing, bail
178
  }
179
- return false;
180
  }
181
  }
182
  ?>
1
  <?php
2
  require_once('wfUtils.php');
3
  class wfCrawl {
4
+ const GOOGLE_BOT_VERIFIED = 'verified';
5
+ const GOOGLE_BOT_FAKE = 'fakeBot';
6
+ const GOOGLE_BOT_UNDETERMINED = 'undetermined';
7
+
8
  public static function isCrawler($UA){
9
  $browscap = new wfBrowscap();
10
  $b = $browscap->getBrowser($UA);
124
  $verified[$ip] = true;
125
  return $verified[$ip];
126
  }
127
+ $noc1Status = self::verifyGooglebotViaNOC1($ip);
128
+ if ($noc1Status == self::GOOGLE_BOT_VERIFIED) {
129
  $verified[$ip] = true;
130
  return $verified[$ip];
131
  }
132
+ else if ($noc1Status == self::GOOGLE_BOT_FAKE) {
133
+ $verified[$ip] = false;
134
+ return $verified[$ip];
135
+ }
136
+
137
+ return true; //We were unable to successfully validate Googlebot status so default to being permissive
138
  }
139
  $verified[$ip] = false;
140
  return $verified[$ip];
141
  }
142
 
143
  /**
144
+ * Attempts to verify whether an IP claiming to be Googlebot is actually Googlebot.
145
+ *
146
  * @param string|null $ip
147
+ * @return string
148
  */
149
  public static function verifyGooglebotViaNOC1($ip = null) {
150
  global $wpdb;
163
  $patternSig,
164
  WORDFENCE_CRAWLER_VERIFY_CACHE_TIME);
165
  if ($status === 'verified') {
166
+ return self::GOOGLE_BOT_VERIFIED;
167
  } else if ($status === 'fakeBot') {
168
+ return self::GOOGLE_BOT_FAKE;
169
  }
170
 
171
  $api = new wfAPI(wfConfig::get('apiKey'), wfUtils::getWPVersion());
179
  values (%s, UNHEX(MD5('%s')), '%s', unix_timestamp())
180
  ON DUPLICATE KEY UPDATE status='%3\$s', lastUpdate=unix_timestamp()",
181
  $IPn, $patternSig, 'verified');
182
+ return self::GOOGLE_BOT_VERIFIED;
183
  } else {
184
  $db->queryWrite("insert into $table (IP, patternSig, status, lastUpdate)
185
  values (%s, UNHEX(MD5('%s')), '%s', unix_timestamp())
186
  ON DUPLICATE KEY UPDATE status='%3\$s', lastUpdate=unix_timestamp()",
187
  $IPn, $patternSig, 'fakeBot');
188
+ self::GOOGLE_BOT_FAKE;
189
  }
190
  } catch (Exception $e) {
191
  // Do nothing, bail
192
  }
193
+ return self::GOOGLE_BOT_UNDETERMINED;
194
  }
195
  }
196
  ?>
lib/wfIssues.php CHANGED
@@ -233,8 +233,15 @@ class wfIssues {
233
  public function deleteIgnored(){
234
  $this->getDB()->queryWrite("delete from " . $this->issuesTable . " where status='ignoreP' or status='ignoreC'");
235
  }
236
- public function deleteNew(){
237
- $this->getDB()->queryWrite("delete from " . $this->issuesTable . " where status='new'");
 
 
 
 
 
 
 
238
  }
239
  public function ignoreAllNew(){
240
  $this->getDB()->queryWrite("update " . $this->issuesTable . " set status='ignoreC' where status='new'");
233
  public function deleteIgnored(){
234
  $this->getDB()->queryWrite("delete from " . $this->issuesTable . " where status='ignoreP' or status='ignoreC'");
235
  }
236
+ public function deleteNew($types = null) {
237
+ if (!is_array($types)) {
238
+ $this->getDB()->queryWrite("DELETE FROM {$this->issuesTable} WHERE status = 'new'");
239
+ }
240
+ else {
241
+ $query = "DELETE FROM {$this->issuesTable} WHERE status = 'new' AND type IN (" . implode(',', array_fill(0, count($types), "'%s'")) . ")";
242
+ array_unshift($types, $query);
243
+ call_user_func_array(array($this->getDB(), 'queryWrite'), $types);
244
+ }
245
  }
246
  public function ignoreAllNew(){
247
  $this->getDB()->queryWrite("update " . $this->issuesTable . " set status='ignoreC' where status='new'");
lib/wfScan.php CHANGED
@@ -1,8 +1,27 @@
1
  <?php
2
  class wfScan {
 
 
 
3
  public static $debugMode = false;
4
  public static $errorHandlingOn = true;
5
  private static $peakMemAtStart = 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  public static function wfScanMain(){
7
  self::$peakMemAtStart = memory_get_peak_usage(true);
8
  $db = new wfDB();
@@ -85,9 +104,59 @@ class wfScan {
85
  exit();
86
  }
87
  } else {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  wfIssues::statusPrep(); //Re-initializes all status counters
89
- $scan = new wfScanEngine();
90
- $scan->deleteNewIssues();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
  }
92
  try {
93
  $scan->go();
@@ -124,6 +193,9 @@ class wfScan {
124
  $peakMemory = self::logPeakMemory();
125
  self::status(2, 'info', "Wordfence used " . wfUtils::formatBytes($peakMemory - self::$peakMemAtStart) . " of memory for scan. Server peak memory usage was: " . wfUtils::formatBytes($peakMemory));
126
  }
 
 
 
127
  private static function logPeakMemory(){
128
  $oldPeak = wfConfig::get('wfPeakMemory', 0, false);
129
  $peak = memory_get_peak_usage(true);
1
  <?php
2
  class wfScan {
3
+ const SCAN_SCHEDULING_MODE_AUTOMATIC = 'auto';
4
+ const SCAN_SCHEDULING_MODE_MANUAL = 'manual';
5
+
6
  public static $debugMode = false;
7
  public static $errorHandlingOn = true;
8
  private static $peakMemAtStart = 0;
9
+
10
+ public static function scanSchedulingMode() {
11
+ $sched = wfConfig::get_ser('scanSched', array());
12
+ if (wfConfig::get('isPaid') && wfConfig::get('schedMode') == 'manual' && is_array($sched) && is_array($sched[0])) {
13
+ return self::SCAN_SCHEDULING_MODE_MANUAL;
14
+ }
15
+ return self::SCAN_SCHEDULING_MODE_AUTOMATIC;
16
+ }
17
+ public static function isAutoScanSchedule() { return self::scanSchedulingMode() == self::SCAN_SCHEDULING_MODE_AUTOMATIC; }
18
+ public static function isManualScanSchedule() { return self::scanSchedulingMode() == self::SCAN_SCHEDULING_MODE_MANUAL; }
19
+
20
+ public static function shouldRunScan($scanMode) {
21
+ $jobs = wfScanEngine::jobsForScanMode($scanMode);
22
+ return count($jobs) > 0;
23
+ }
24
+
25
  public static function wfScanMain(){
26
  self::$peakMemAtStart = memory_get_peak_usage(true);
27
  $db = new wfDB();
104
  exit();
105
  }
106
  } else {
107
+ $scanMode = wfScanEngine::SCAN_MODE_FULL;
108
+ if (isset($_GET['scanMode']) && self::_isValidScanMode($_GET['scanMode'])) {
109
+ $scanMode = $_GET['scanMode'];
110
+ }
111
+
112
+ $delay = -1;
113
+ $isScheduled = false;
114
+ $originalScanStart = wfConfig::get('originalScheduledScanStart', 0);
115
+ $lastScanStart = wfConfig::get('lastScheduledScanStart', 0);
116
+ $minimumFrequency = (wfScan::isManualScanSchedule() ? 1800 : 43200);
117
+ if ($lastScanStart && (time() - $lastScanStart) < $minimumFrequency) {
118
+ $isScheduled = true;
119
+
120
+ if ($originalScanStart > 0) {
121
+ $delay = max($lastScanStart - $originalScanStart, 0);
122
+ }
123
+ }
124
+
125
  wfIssues::statusPrep(); //Re-initializes all status counters
126
+
127
+ if ($scanMode == wfScanEngine::SCAN_MODE_FULL) {
128
+ wordfence::status(1, 'info', "Contacting Wordfence to initiate scan");
129
+ $wp_version = wfUtils::getWPVersion();
130
+ $apiKey = wfConfig::get('apiKey');
131
+ $api = new wfAPI($apiKey, $wp_version);
132
+ $response = $api->call('log_scan', array(), array('delay' => $delay, 'scheduled' => (int) $isScheduled, 'mode' => wfConfig::get('schedMode')/*, 'forcedefer' => 1*/));
133
+
134
+ if (!wfScan::isManualScanSchedule() && $isScheduled) {
135
+ if (isset($response['defer'])) {
136
+ $defer = (int) $response['defer'];
137
+ wordfence::status(2, 'info', "Deferring scheduled scan by " . wfUtils::makeDuration($defer));
138
+ wfConfig::set('lastScheduledScanStart', 0);
139
+ wfConfig::set('lastScanCompleted', 'ok');
140
+ wfConfig::set('lastScanFailureType', false);
141
+ wfConfig::set_ser('wfStatusStartMsgs', array());
142
+ $i = new wfIssues();
143
+ $i->setScanTimeNow();
144
+ wfScanEngine::refreshScanNotification($i);
145
+ wordfence::scheduleSingleScan(time() + $defer, $originalScanStart);
146
+ wfUtils::clearScanLock();
147
+ exit();
148
+ }
149
+ }
150
+
151
+ $malwarePrefixesHash = (isset($response['malwarePrefixes']) ? $response['malwarePrefixes'] : '');
152
+
153
+ $scan = new wfScanEngine($malwarePrefixesHash, $scanMode);
154
+ $scan->deleteNewIssues();
155
+ }
156
+ else {
157
+ wordfence::status(1, 'info', "Initiating quick scan");
158
+ $scan = new wfScanEngine('', $scanMode);
159
+ }
160
  }
161
  try {
162
  $scan->go();
193
  $peakMemory = self::logPeakMemory();
194
  self::status(2, 'info', "Wordfence used " . wfUtils::formatBytes($peakMemory - self::$peakMemAtStart) . " of memory for scan. Server peak memory usage was: " . wfUtils::formatBytes($peakMemory));
195
  }
196
+ private static function _isValidScanMode($scanMode) {
197
+ return ($scanMode == wfScanEngine::SCAN_MODE_QUICK || $scanMode == wfScanEngine::SCAN_MODE_FULL);
198
+ }
199
  private static function logPeakMemory(){
200
  $oldPeak = wfConfig::get('wfPeakMemory', 0, false);
201
  $peak = memory_get_peak_usage(true);
lib/wfScanEngine.php CHANGED
@@ -7,6 +7,9 @@ require_once('wfIssues.php');
7
  require_once('wfDB.php');
8
  require_once('wfUtils.php');
9
  class wfScanEngine {
 
 
 
10
  public $api = false;
11
  private $dictWords = array();
12
  private $forkRequested = false;
@@ -43,6 +46,8 @@ class wfScanEngine {
43
  private $gsbMultisiteBlogOffset = 0;
44
  private $updateCheck = false;
45
  private $pluginRepoStatus = array();
 
 
46
 
47
  /**
48
  * @var wordfenceDBScanner
@@ -118,11 +123,43 @@ class wfScanEngine {
118
  }
119
  }
120
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
 
122
  public function __sleep(){ //Same order here as above for properties that are included in serialization
123
- return array('hasher', 'jobList', 'i', 'wp_version', 'apiKey', 'startTime', 'maxExecTime', 'publicScanEnabled', 'fileContentsResults', 'scanner', 'scanQueue', 'hoover', 'scanData', 'statusIDX', 'userPasswdQueue', 'passwdHasIssues', 'suspectedFiles', 'dbScanner', 'knownFilesLoader', 'metrics', 'checkHowGetIPsRequestTime', 'gsbMultisiteBlogOffset', 'updateCheck', 'pluginRepoStatus');
124
  }
125
- public function __construct(){
126
  $this->startTime = time();
127
  $this->recordMetric('scan', 'start', $this->startTime);
128
  $this->maxExecTime = self::getMaxExecutionTime();
@@ -131,34 +168,31 @@ class wfScanEngine {
131
  $this->wp_version = wfUtils::getWPVersion();
132
  $this->apiKey = wfConfig::get('apiKey');
133
  $this->api = new wfAPI($this->apiKey, $this->wp_version);
 
134
  include('wfDict.php'); //$dictWords
135
  $this->dictWords = $dictWords;
136
- $this->jobList[] = 'checkSpamvertized';
137
- $this->jobList[] = 'checkSpamIP';
138
- $this->jobList[] = 'checkGSB_init';
139
- $this->jobList[] = 'checkGSB_main';
140
- $this->jobList[] = 'checkGSB_finish';
141
- $this->jobList[] = 'checkHowGetIPs_init';
142
- $this->jobList[] = 'checkHowGetIPs_main';
143
- $this->jobList[] = 'knownFiles_init';
144
- $this->jobList[] = 'knownFiles_main';
145
- $this->jobList[] = 'knownFiles_finish';
146
- foreach (array('knownFiles', 'checkReadableConfig', 'fileContents', 'suspectedFiles',
147
- // 'wpscan_fullPathDisclosure', 'wpscan_directoryListingEnabled',
148
- 'posts', 'comments', 'passwds', 'dns', 'diskSpace', 'oldVersions', 'suspiciousAdminUsers') as $scanType) {
149
- if (wfConfig::get('scansEnabled_' . $scanType)) {
150
- if (method_exists($this, 'scan_' . $scanType . '_init')) {
151
- foreach (array('init', 'main', 'finish') as $op) {
152
- $this->jobList[] = $scanType . '_' . $op;
153
- };
154
- } else if (method_exists($this, 'scan_' . $scanType)) {
155
- $this->jobList[] = $scanType;
156
  }
157
  }
 
 
 
158
  }
159
  }
160
- public function deleteNewIssues(){
161
- $this->i->deleteNew();
 
 
 
 
 
 
162
  }
163
  public function __wakeup(){
164
  $this->cycleStartTime = time();
@@ -166,6 +200,9 @@ class wfScanEngine {
166
  include('wfDict.php'); //$dictWords
167
  $this->dictWords = $dictWords;
168
  }
 
 
 
169
  public function go(){
170
  try {
171
  self::checkForKill();
@@ -177,9 +214,11 @@ class wfScanEngine {
177
  $this->i->setScanTimeNow();
178
  //scan ID only incremented at end of scan to make UI load new results
179
  $this->emailNewIssues();
180
- $this->recordMetric('scan', 'duration', (time() - $this->startTime));
181
- $this->recordMetric('scan', 'memory', wfConfig::get('wfPeakMemory', 0, false));
182
- $this->submitMetrics();
 
 
183
 
184
  wfScanEngine::refreshScanNotification($this->i);
185
  }
@@ -283,7 +322,7 @@ class wfScanEngine {
283
  wordfence::status(4, 'info', "Entered fork()");
284
  if(wfConfig::set_ser('wfsd_engine', $this, true, wfConfig::DONT_AUTOLOAD)){
285
  wordfence::status(4, 'info', "Calling startScan(true)");
286
- self::startScan(true);
287
  } //Otherwise there was an error so don't start another scan.
288
  exit(0);
289
  }
@@ -322,7 +361,12 @@ class wfScanEngine {
322
  }
323
  $summary = $this->i->getSummaryItems();
324
  $this->status(1, 'info', '-------------------');
325
- $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 " . wfUtils::makeDuration(time() - $this->startTime, true) . ".");
 
 
 
 
 
326
 
327
  $ignoredText = '';
328
  if ($this->i->totalIgnoredIssues > 0) {
@@ -552,6 +596,10 @@ class wfScanEngine {
552
  wfIssues::statusEnd($this->statusIDX['checkHowGetIPs'], $haveIssues);
553
  }
554
  }
 
 
 
 
555
 
556
  private function scan_checkReadableConfig() {
557
  $haveIssues = wfIssues::STATUS_SECURE;
@@ -699,8 +747,6 @@ class wfScanEngine {
699
  }
700
  }
701
  private function scan_knownFiles_init(){
702
- $this->status(1, 'info', "Contacting Wordfence to initiate scan");
703
- $response = $this->api->call('log_scan', array(), array());
704
  $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');
705
  $baseContents = scandir(ABSPATH);
706
  if(! is_array($baseContents)){
@@ -733,8 +779,7 @@ class wfScanEngine {
733
  $this->status(2, 'info', "Found " . sizeof($knownFilesThemes) . " themes");
734
  $this->i->updateSummaryItem('totalThemes', sizeof($knownFilesThemes));
735
 
736
- $malwarePrefixesHash = (isset($response['malwarePrefixes']) ? wfUtils::hex2bin($response['malwarePrefixes']) : '');
737
- $this->hasher = new wordfenceHash(strlen(ABSPATH), ABSPATH, $includeInKnownFilesScan, $knownFilesThemes, $knownFilesPlugins, $this, $malwarePrefixesHash);
738
  }
739
  private function scan_knownFiles_main(){
740
  $this->hasher->run($this); //Include this so we can call addIssue and ->api->
@@ -1418,8 +1463,13 @@ class wfScanEngine {
1418
  $this->statusIDX['oldVersions'] = wfIssues::statusStart("Scanning for old themes, plugins and core files");
1419
 
1420
  $this->updateCheck = new wfUpdateCheck();
1421
- $this->updateCheck->checkAllUpdates(false);
1422
- $this->updateCheck->checkAllVulnerabilities();
 
 
 
 
 
1423
 
1424
  foreach ($this->updateCheck->getPluginSlugs() as $slug) {
1425
  $this->pluginRepoStatus[$slug] = false;
@@ -1436,6 +1486,10 @@ class wfScanEngine {
1436
  }
1437
 
1438
  private function scan_oldVersions_main() {
 
 
 
 
1439
  if (!function_exists('plugins_api')) {
1440
  require_once(ABSPATH . 'wp-admin/includes/plugin-install.php');
1441
  }
@@ -1480,6 +1534,10 @@ class wfScanEngine {
1480
 
1481
  private function scan_oldVersions_finish() {
1482
  $haveIssues = wfIssues::STATUS_SECURE;
 
 
 
 
1483
 
1484
  // WordPress core updates needed
1485
  if ($this->updateCheck->needsCoreUpdate()) {
@@ -1531,95 +1589,112 @@ class wfScanEngine {
1531
  }
1532
  }
1533
 
1534
- //Abandoned plugins
1535
- foreach ($this->pluginRepoStatus as $slug => $status) {
1536
- if ($status !== false && !is_wp_error($status) && property_exists($status, 'last_updated')) {
1537
- $lastUpdateTimestamp = strtotime($status->last_updated);
1538
- if ($lastUpdateTimestamp > 0 && (time() - $lastUpdateTimestamp) > 63072000 /* ~2 years */) {
1539
- $statusArray = (array) $status;
1540
- $statusArray['dateUpdated'] = wfUtils::formatLocalTime(get_option('date_format'), $lastUpdateTimestamp);
1541
- $severity = 2; //Warning
1542
- $statusArray['abandoned'] = true;
1543
- $statusArray['vulnerable'] = false;
1544
- if ($this->updateCheck->isPluginVulnerable($slug, $statusArray['version'])) {
1545
- $severity = 1; //Critical
1546
- $statusArray['vulnerable'] = true;
1547
- }
1548
-
1549
- if (isset($allPlugins[$slug])) {
1550
- $statusArray['wpURL'] = $allPlugins[$slug]['wpURL'];
1551
- }
1552
-
1553
- $key = "wfPluginAbandoned {$slug} {$statusArray['version']}";
1554
- $shortMsg = 'The Plugin "' . $statusArray['name'] . '" appears to be abandoned.';
1555
- $longMsg = 'It was last updated ' . wfUtils::makeTimeAgo(time() - $lastUpdateTimestamp) . ' ago.';
1556
- if ($statusArray['vulnerable']) {
1557
- $longMsg .= ' It has unpatched security issues and may have compatibility problems with the current version of WordPress.';
1558
- }
1559
- else {
1560
- $longMsg .= ' It may have compatibility problems with the current version of WordPress or unknown security issues.';
1561
- }
1562
- $longMsg .= ' <a href="https://docs.wordfence.com/en/Understanding_scan_results#Plugin_appears_to_be_abandoned" target="_blank" rel="noopener noreferrer">Get more information.</a>';
1563
- $added = $this->addIssue('wfPluginAbandoned', $severity, $key, $key, $shortMsg, $longMsg, $statusArray);
1564
- if ($added == wfIssues::ISSUE_ADDED || $added == wfIssues::ISSUE_UPDATED) { $haveIssues = wfIssues::STATUS_PROBLEM; }
1565
- else if ($haveIssues != wfIssues::STATUS_PROBLEM && ($added == wfIssues::ISSUE_IGNOREP || $added == wfIssues::ISSUE_IGNOREC)) { $haveIssues = wfIssues::STATUS_IGNORED; }
1566
-
1567
- unset($allPlugins[$slug]);
1568
- }
1569
- }
1570
- else if ($status !== false && is_wp_error($status) && isset($status->error_data['plugins_api_failed']) && $status->error_data['plugins_api_failed'] == 'N;') { //The plugin does not exist in the wp.org repo
1571
- $knownFiles = $this->getKnownFilesLoader()->getKnownFiles();
1572
- $requestedPlugins = $this->getPlugins();
1573
- foreach ($requestedPlugins as $key => $data) {
1574
- $testKey = trailingslashit($data['FullDir']) . '.';
1575
- if ($data['ShortDir'] == $slug && isset($knownFiles['plugins'][$testKey])) { //It existed in the repo at some point and was removed
1576
- $pluginFile = wfUtils::getPluginBaseDir() . $key;
1577
- $pluginData = get_plugin_data($pluginFile);
1578
- $pluginData['wpRemoved'] = true;
1579
- $pluginData['vulnerable'] = false;
1580
- if ($this->updateCheck->isPluginVulnerable($slug, $pluginData['Version'])) {
1581
- $pluginData['vulnerable'] = true;
1582
  }
1583
 
1584
- $key = "wfPluginRemoved {$slug} {$pluginData['Version']}";
1585
- $shortMsg = 'The Plugin "' . $pluginData['Name'] . '" has been removed from wordpress.org.';
1586
- if ($pluginData['vulnerable']) {
1587
- $longMsg = 'It has unpatched security issues and may have compatibility problems with the current version of WordPress.';
 
1588
  }
1589
  else {
1590
- $longMsg = 'It may have compatibility problems with the current version of WordPress or unknown security issues.';
1591
  }
1592
- $longMsg .= ' <a href="https://docs.wordfence.com/en/Understanding_scan_results#Plugin_has_been_removed_from_wordpress.org" target="_blank" rel="noopener noreferrer">Get more information.</a>';
1593
- $added = $this->addIssue('wfPluginRemoved', 1, $key, $key, $shortMsg, $longMsg, $pluginData);
1594
  if ($added == wfIssues::ISSUE_ADDED || $added == wfIssues::ISSUE_UPDATED) { $haveIssues = wfIssues::STATUS_PROBLEM; }
1595
  else if ($haveIssues != wfIssues::STATUS_PROBLEM && ($added == wfIssues::ISSUE_IGNOREP || $added == wfIssues::ISSUE_IGNOREC)) { $haveIssues = wfIssues::STATUS_IGNORED; }
1596
 
1597
  unset($allPlugins[$slug]);
1598
  }
1599
  }
1600
- }
1601
- }
1602
-
1603
- //Other vulnerable plugins
1604
- //Disabled until we improve the data source to weed out false positives
1605
- /*if (count($allPlugins) > 0) {
1606
- foreach ($allPlugins as $plugin) {
1607
- if (!isset($plugin['vulnerable']) || !$plugin['vulnerable']) {
1608
- continue;
1609
- }
1610
-
1611
- $key = 'wfPluginVulnerable' . ' ' . $plugin['pluginFile'] . ' ' . $plugin['Version'];
1612
- $shortMsg = "The Plugin \"" . $plugin['Name'] . "\" has an unpatched security vulnerability.";
1613
- $longMsg = 'To protect your site from this vulnerability, the safest option is to deactivate and completely remove ' . esc_html($plugin['Name']) . ' until the developer releases a security fix. <a href="https://docs.wordfence.com/en/Understanding_scan_results#Plugin_has_an_unpatched_security_vulnerability" target="_blank" rel="noopener noreferrer">Get more information.</a>';
1614
- $added = $this->addIssue('wfPluginVulnerable', 1, $key, $key, $shortMsg, $longMsg, $plugin);
1615
- if ($added == wfIssues::ISSUE_ADDED || $added == wfIssues::ISSUE_UPDATED) { $haveIssues = wfIssues::STATUS_PROBLEM; }
1616
- else if ($haveIssues != wfIssues::STATUS_PROBLEM && ($added == wfIssues::ISSUE_IGNOREP || $added == wfIssues::ISSUE_IGNOREC)) { $haveIssues = wfIssues::STATUS_IGNORED; }
1617
-
1618
- if (isset($plugin['slug'])) {
1619
- unset($allPlugins[$plugin['slug']]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1620
  }
1621
  }
1622
- }*/
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1623
 
1624
  $this->updateCheck = false;
1625
  $this->pluginRepoStatus = array();
@@ -1679,7 +1754,7 @@ class wfScanEngine {
1679
  throw new Exception("Scan was killed on administrator request.");
1680
  }
1681
  }
1682
- public static function startScan($isFork = false){
1683
  if(! $isFork){ //beginning of scan
1684
  wfConfig::inc('totalScansRun');
1685
  wfConfig::set('wfKillRequested', 0, wfConfig::DONT_AUTOLOAD);
@@ -1705,7 +1780,7 @@ class wfScanEngine {
1705
  wfConfig::set('currentCronKey', time() . ',' . $cronKey);
1706
  if( (! wfConfig::get('startScansRemotely', false)) && (! is_wp_error($testResult)) && (is_array($testResult) || $testResult instanceof ArrayAccess) && strstr($testResult['body'], 'WFSCANTESTOK') !== false){
1707
  //ajax requests can be sent by the server to itself
1708
- $cronURL = 'admin-ajax.php?action=wordfence_doScan&isFork=' . ($isFork ? '1' : '0') . '&cronKey=' . $cronKey;
1709
  $cronURL = admin_url($cronURL);
1710
  $headers = array('Referer' => false/*, 'Cookie' => 'XDEBUG_SESSION=1'*/);
1711
  wordfence::status(4, 'info', "Starting cron with normal ajax at URL $cronURL");
@@ -1719,7 +1794,7 @@ class wfScanEngine {
1719
  } else {
1720
  $cronURL = admin_url('admin-ajax.php');
1721
  $cronURL = preg_replace('/^(https?:\/\/)/i', '$1noc1.wordfence.com/scanp/', $cronURL);
1722
- $cronURL .= '?action=wordfence_doScan&isFork=' . ($isFork ? '1' : '0') . '&cronKey=' . $cronKey;
1723
  $headers = array();
1724
  wordfence::status(4, 'info', "Starting cron via proxy at URL $cronURL");
1725
 
7
  require_once('wfDB.php');
8
  require_once('wfUtils.php');
9
  class wfScanEngine {
10
+ const SCAN_MODE_FULL = 'full';
11
+ const SCAN_MODE_QUICK = 'quick';
12
+
13
  public $api = false;
14
  private $dictWords = array();
15
  private $forkRequested = false;
46
  private $gsbMultisiteBlogOffset = 0;
47
  private $updateCheck = false;
48
  private $pluginRepoStatus = array();
49
+ private $malwarePrefixesHash;
50
+ private $scanMode = self::SCAN_MODE_FULL;
51
 
52
  /**
53
  * @var wordfenceDBScanner
123
  }
124
  }
125
  }
126
+
127
+ /**
128
+ * Returns an array of the jobs that are enabled for the given scan mode.
129
+ *
130
+ * @param $scanMode
131
+ * @return array
132
+ */
133
+ public static function jobsForScanMode($scanMode) {
134
+ $jobs = array();
135
+ if ($scanMode == self::SCAN_MODE_FULL) {
136
+ $jobs[] = 'checkSpamvertized';
137
+ $jobs[] = 'checkSpamIP';
138
+ $jobs[] = 'checkGSB';
139
+ $jobs[] = 'checkHowGetIPs';
140
+ $jobs[] = 'knownFiles';
141
+ foreach (array('knownFiles', 'checkReadableConfig', 'fileContents', 'suspectedFiles',
142
+ // 'wpscan_fullPathDisclosure', 'wpscan_directoryListingEnabled',
143
+ 'posts', 'comments', 'passwds', 'dns', 'diskSpace', 'oldVersions', 'suspiciousAdminUsers') as $job) {
144
+ if (wfConfig::get('scansEnabled_' . $job)) {
145
+ $jobs[] = $job;
146
+ }
147
+ }
148
+ }
149
+ else if ($scanMode == self::SCAN_MODE_QUICK) {
150
+ foreach (array('oldVersions') as $job) {
151
+ if (wfConfig::get('scansEnabled_' . $job)) {
152
+ $jobs[] = $job;
153
+ }
154
+ }
155
+ }
156
+ return $jobs;
157
+ }
158
 
159
  public function __sleep(){ //Same order here as above for properties that are included in serialization
160
+ return array('hasher', 'jobList', 'i', 'wp_version', 'apiKey', 'startTime', 'maxExecTime', 'publicScanEnabled', 'fileContentsResults', 'scanner', 'scanQueue', 'hoover', 'scanData', 'statusIDX', 'userPasswdQueue', 'passwdHasIssues', 'suspectedFiles', 'dbScanner', 'knownFilesLoader', 'metrics', 'checkHowGetIPsRequestTime', 'gsbMultisiteBlogOffset', 'updateCheck', 'pluginRepoStatus', 'malwarePrefixesHash', 'scanMode');
161
  }
162
+ public function __construct($malwarePrefixesHash = '', $scanMode = self::SCAN_MODE_FULL) {
163
  $this->startTime = time();
164
  $this->recordMetric('scan', 'start', $this->startTime);
165
  $this->maxExecTime = self::getMaxExecutionTime();
168
  $this->wp_version = wfUtils::getWPVersion();
169
  $this->apiKey = wfConfig::get('apiKey');
170
  $this->api = new wfAPI($this->apiKey, $this->wp_version);
171
+ $this->malwarePrefixesHash = $malwarePrefixesHash;
172
  include('wfDict.php'); //$dictWords
173
  $this->dictWords = $dictWords;
174
+ $this->scanMode = $scanMode;
175
+
176
+ $jobs = self::jobsForScanMode($this->scanMode);
177
+ foreach ($jobs as $job) {
178
+ if (method_exists($this, 'scan_' . $job . '_init')) {
179
+ foreach (array('init', 'main', 'finish') as $op) {
180
+ $this->jobList[] = $job . '_' . $op;
 
 
 
 
 
 
 
 
 
 
 
 
 
181
  }
182
  }
183
+ else if (method_exists($this, 'scan_' . $job)) {
184
+ $this->jobList[] = $job;
185
+ }
186
  }
187
  }
188
+
189
+ /**
190
+ * Deletes all new issues. To only delete specific types, provide an array of issue types.
191
+ *
192
+ * @param null|array $types
193
+ */
194
+ public function deleteNewIssues($types = null) {
195
+ $this->i->deleteNew($types);
196
  }
197
  public function __wakeup(){
198
  $this->cycleStartTime = time();
200
  include('wfDict.php'); //$dictWords
201
  $this->dictWords = $dictWords;
202
  }
203
+ public function isFullScan() {
204
+ return $this->scanMode == self::SCAN_MODE_FULL;
205
+ }
206
  public function go(){
207
  try {
208
  self::checkForKill();
214
  $this->i->setScanTimeNow();
215
  //scan ID only incremented at end of scan to make UI load new results
216
  $this->emailNewIssues();
217
+ if ($this->isFullScan()) {
218
+ $this->recordMetric('scan', 'duration', (time() - $this->startTime));
219
+ $this->recordMetric('scan', 'memory', wfConfig::get('wfPeakMemory', 0, false));
220
+ $this->submitMetrics();
221
+ }
222
 
223
  wfScanEngine::refreshScanNotification($this->i);
224
  }
322
  wordfence::status(4, 'info', "Entered fork()");
323
  if(wfConfig::set_ser('wfsd_engine', $this, true, wfConfig::DONT_AUTOLOAD)){
324
  wordfence::status(4, 'info', "Calling startScan(true)");
325
+ self::startScan(true, $this->scanMode);
326
  } //Otherwise there was an error so don't start another scan.
327
  exit(0);
328
  }
361
  }
362
  $summary = $this->i->getSummaryItems();
363
  $this->status(1, 'info', '-------------------');
364
+ if ($this->isFullScan()) {
365
+ $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 " . wfUtils::makeDuration(time() - $this->startTime, true) . ".");
366
+ }
367
+ else {
368
+ $this->status(1, 'info', "Quick Scan Complete. Scanned in " . wfUtils::makeDuration(time() - $this->startTime, true) . ".");
369
+ }
370
 
371
  $ignoredText = '';
372
  if ($this->i->totalIgnoredIssues > 0) {
596
  wfIssues::statusEnd($this->statusIDX['checkHowGetIPs'], $haveIssues);
597
  }
598
  }
599
+
600
+ private function scan_checkHowGetIPs_finish() {
601
+ /* Do nothing */
602
+ }
603
 
604
  private function scan_checkReadableConfig() {
605
  $haveIssues = wfIssues::STATUS_SECURE;
747
  }
748
  }
749
  private function scan_knownFiles_init(){
 
 
750
  $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');
751
  $baseContents = scandir(ABSPATH);
752
  if(! is_array($baseContents)){
779
  $this->status(2, 'info', "Found " . sizeof($knownFilesThemes) . " themes");
780
  $this->i->updateSummaryItem('totalThemes', sizeof($knownFilesThemes));
781
 
782
+ $this->hasher = new wordfenceHash(strlen(ABSPATH), ABSPATH, $includeInKnownFilesScan, $knownFilesThemes, $knownFilesPlugins, $this, wfUtils::hex2bin($this->malwarePrefixesHash));
 
783
  }
784
  private function scan_knownFiles_main(){
785
  $this->hasher->run($this); //Include this so we can call addIssue and ->api->
1463
  $this->statusIDX['oldVersions'] = wfIssues::statusStart("Scanning for old themes, plugins and core files");
1464
 
1465
  $this->updateCheck = new wfUpdateCheck();
1466
+ if ($this->isFullScan()) {
1467
+ $this->updateCheck->checkAllUpdates(false);
1468
+ $this->updateCheck->checkAllVulnerabilities();
1469
+ }
1470
+ else {
1471
+ $this->updateCheck->checkAllUpdates();
1472
+ }
1473
 
1474
  foreach ($this->updateCheck->getPluginSlugs() as $slug) {
1475
  $this->pluginRepoStatus[$slug] = false;
1486
  }
1487
 
1488
  private function scan_oldVersions_main() {
1489
+ if (!$this->isFullScan()) {
1490
+ return;
1491
+ }
1492
+
1493
  if (!function_exists('plugins_api')) {
1494
  require_once(ABSPATH . 'wp-admin/includes/plugin-install.php');
1495
  }
1534
 
1535
  private function scan_oldVersions_finish() {
1536
  $haveIssues = wfIssues::STATUS_SECURE;
1537
+
1538
+ if (!$this->isFullScan()) {
1539
+ $this->deleteNewIssues(array('wfUpgrade', 'wfPluginUpgrade', 'wfThemeUpgrade'));
1540
+ }
1541
 
1542
  // WordPress core updates needed
1543
  if ($this->updateCheck->needsCoreUpdate()) {
1589
  }
1590
  }
1591
 
1592
+ if ($this->isFullScan()) {
1593
+ //Abandoned plugins
1594
+ foreach ($this->pluginRepoStatus as $slug => $status) {
1595
+ if ($status !== false && !is_wp_error($status) && property_exists($status, 'last_updated')) {
1596
+ $lastUpdateTimestamp = strtotime($status->last_updated);
1597
+ if ($lastUpdateTimestamp > 0 && (time() - $lastUpdateTimestamp) > 63072000 /* ~2 years */) {
1598
+ $statusArray = (array) $status;
1599
+ $statusArray['dateUpdated'] = wfUtils::formatLocalTime(get_option('date_format'), $lastUpdateTimestamp);
1600
+ $severity = 2; //Warning
1601
+ $statusArray['abandoned'] = true;
1602
+ $statusArray['vulnerable'] = false;
1603
+ $vulnerable = $this->updateCheck->isPluginVulnerable($slug, $statusArray['version']);
1604
+ if ($vulnerable) {
1605
+ $severity = 1; //Critical
1606
+ $statusArray['vulnerable'] = true;
1607
+ if (is_string($vulnerable)) {
1608
+ $statusArray['vulnerabilityLink'] = $vulnerable;
1609
+ }
1610
+ }
1611
+
1612
+ if (isset($allPlugins[$slug])) {
1613
+ $statusArray['wpURL'] = $allPlugins[$slug]['wpURL'];
1614
+ }
1615
+
1616
+ $testedShort = '';
1617
+ $testedLong = '';
1618
+ if (isset($statusArray['tested'])) {
1619
+ $testedShort = ', tested to WP ' . $statusArray['tested'];
1620
+ $testedLong = ' and tested up to WordPress ' . esc_html($statusArray['tested']);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1621
  }
1622
 
1623
+ $key = "wfPluginAbandoned {$slug} {$statusArray['version']}";
1624
+ $shortMsg = 'The Plugin "' . $statusArray['name'] . '" appears to be abandoned (updated ' . wfUtils::formatLocalTime(get_option('date_format'), $lastUpdateTimestamp) . "{$testedShort}).";
1625
+ $longMsg = 'It was last updated ' . wfUtils::makeTimeAgo(time() - $lastUpdateTimestamp) . " ago{$testedLong}.";
1626
+ if ($statusArray['vulnerable']) {
1627
+ $longMsg .= ' It has unpatched security issues and may have compatibility problems with the current version of WordPress.';
1628
  }
1629
  else {
1630
+ $longMsg .= ' It may have compatibility problems with the current version of WordPress or unknown security issues.';
1631
  }
1632
+ $longMsg .= ' <a href="https://docs.wordfence.com/en/Understanding_scan_results#Plugin_appears_to_be_abandoned" target="_blank" rel="noopener noreferrer">Get more information.</a>';
1633
+ $added = $this->addIssue('wfPluginAbandoned', $severity, $key, $key, $shortMsg, $longMsg, $statusArray);
1634
  if ($added == wfIssues::ISSUE_ADDED || $added == wfIssues::ISSUE_UPDATED) { $haveIssues = wfIssues::STATUS_PROBLEM; }
1635
  else if ($haveIssues != wfIssues::STATUS_PROBLEM && ($added == wfIssues::ISSUE_IGNOREP || $added == wfIssues::ISSUE_IGNOREC)) { $haveIssues = wfIssues::STATUS_IGNORED; }
1636
 
1637
  unset($allPlugins[$slug]);
1638
  }
1639
  }
1640
+ else if ($status !== false && is_wp_error($status) && isset($status->error_data['plugins_api_failed']) && $status->error_data['plugins_api_failed'] == 'N;') { //The plugin does not exist in the wp.org repo
1641
+ $knownFiles = $this->getKnownFilesLoader()->getKnownFiles();
1642
+ $requestedPlugins = $this->getPlugins();
1643
+ foreach ($requestedPlugins as $key => $data) {
1644
+ $testKey = trailingslashit($data['FullDir']) . '.';
1645
+ if ($data['ShortDir'] == $slug && isset($knownFiles['plugins'][$testKey])) { //It existed in the repo at some point and was removed
1646
+ $pluginFile = wfUtils::getPluginBaseDir() . $key;
1647
+ $pluginData = get_plugin_data($pluginFile);
1648
+ $pluginData['wpRemoved'] = true;
1649
+ $pluginData['vulnerable'] = false;
1650
+ $vulnerable = $this->updateCheck->isPluginVulnerable($slug, $pluginData['Version']);
1651
+ if ($vulnerable) {
1652
+ $pluginData['vulnerable'] = true;
1653
+ if (is_string($vulnerable)) {
1654
+ $pluginData['vulnerabilityLink'] = $vulnerable;
1655
+ }
1656
+ }
1657
+
1658
+ $key = "wfPluginRemoved {$slug} {$pluginData['Version']}";
1659
+ $shortMsg = 'The Plugin "' . $pluginData['Name'] . '" has been removed from wordpress.org.';
1660
+ if ($pluginData['vulnerable']) {
1661
+ $longMsg = 'It has unpatched security issues and may have compatibility problems with the current version of WordPress.';
1662
+ }
1663
+ else {
1664
+ $longMsg = 'It may have compatibility problems with the current version of WordPress or unknown security issues.';
1665
+ }
1666
+ $longMsg .= ' <a href="https://docs.wordfence.com/en/Understanding_scan_results#Plugin_has_been_removed_from_wordpress.org" target="_blank" rel="noopener noreferrer">Get more information.</a>';
1667
+ $added = $this->addIssue('wfPluginRemoved', 1, $key, $key, $shortMsg, $longMsg, $pluginData);
1668
+ if ($added == wfIssues::ISSUE_ADDED || $added == wfIssues::ISSUE_UPDATED) { $haveIssues = wfIssues::STATUS_PROBLEM; }
1669
+ else if ($haveIssues != wfIssues::STATUS_PROBLEM && ($added == wfIssues::ISSUE_IGNOREP || $added == wfIssues::ISSUE_IGNOREC)) { $haveIssues = wfIssues::STATUS_IGNORED; }
1670
+
1671
+ unset($allPlugins[$slug]);
1672
+ }
1673
+ }
1674
  }
1675
  }
1676
+
1677
+ //Other vulnerable plugins
1678
+ //Disabled until we improve the data source to weed out false positives
1679
+ /*if (count($allPlugins) > 0) {
1680
+ foreach ($allPlugins as $plugin) {
1681
+ if (!isset($plugin['vulnerable']) || !$plugin['vulnerable']) {
1682
+ continue;
1683
+ }
1684
+
1685
+ $key = 'wfPluginVulnerable' . ' ' . $plugin['pluginFile'] . ' ' . $plugin['Version'];
1686
+ $shortMsg = "The Plugin \"" . $plugin['Name'] . "\" has an unpatched security vulnerability.";
1687
+ $longMsg = 'To protect your site from this vulnerability, the safest option is to deactivate and completely remove ' . esc_html($plugin['Name']) . ' until the developer releases a security fix. <a href="https://docs.wordfence.com/en/Understanding_scan_results#Plugin_has_an_unpatched_security_vulnerability" target="_blank" rel="noopener noreferrer">Get more information.</a>';
1688
+ $added = $this->addIssue('wfPluginVulnerable', 1, $key, $key, $shortMsg, $longMsg, $plugin);
1689
+ if ($added == wfIssues::ISSUE_ADDED || $added == wfIssues::ISSUE_UPDATED) { $haveIssues = wfIssues::STATUS_PROBLEM; }
1690
+ else if ($haveIssues != wfIssues::STATUS_PROBLEM && ($added == wfIssues::ISSUE_IGNOREP || $added == wfIssues::ISSUE_IGNOREC)) { $haveIssues = wfIssues::STATUS_IGNORED; }
1691
+
1692
+ if (isset($plugin['slug'])) {
1693
+ unset($allPlugins[$plugin['slug']]);
1694
+ }
1695
+ }
1696
+ }*/
1697
+ }
1698
 
1699
  $this->updateCheck = false;
1700
  $this->pluginRepoStatus = array();
1754
  throw new Exception("Scan was killed on administrator request.");
1755
  }
1756
  }
1757
+ public static function startScan($isFork = false, $scanMode = self::SCAN_MODE_FULL){
1758
  if(! $isFork){ //beginning of scan
1759
  wfConfig::inc('totalScansRun');
1760
  wfConfig::set('wfKillRequested', 0, wfConfig::DONT_AUTOLOAD);
1780
  wfConfig::set('currentCronKey', time() . ',' . $cronKey);
1781
  if( (! wfConfig::get('startScansRemotely', false)) && (! is_wp_error($testResult)) && (is_array($testResult) || $testResult instanceof ArrayAccess) && strstr($testResult['body'], 'WFSCANTESTOK') !== false){
1782
  //ajax requests can be sent by the server to itself
1783
+ $cronURL = 'admin-ajax.php?action=wordfence_doScan&isFork=' . ($isFork ? '1' : '0') . '&scanMode=' . $scanMode . '&cronKey=' . $cronKey;
1784
  $cronURL = admin_url($cronURL);
1785
  $headers = array('Referer' => false/*, 'Cookie' => 'XDEBUG_SESSION=1'*/);
1786
  wordfence::status(4, 'info', "Starting cron with normal ajax at URL $cronURL");
1794
  } else {
1795
  $cronURL = admin_url('admin-ajax.php');
1796
  $cronURL = preg_replace('/^(https?:\/\/)/i', '$1noc1.wordfence.com/scanp/', $cronURL);
1797
+ $cronURL .= '?action=wordfence_doScan&isFork=' . ($isFork ? '1' : '0') . '&scanMode=' . $scanMode . '&cronKey=' . $cronKey;
1798
  $headers = array();
1799
  wordfence::status(4, 'info', "Starting cron via proxy at URL $cronURL");
1800
 
lib/wfUpdateCheck.php CHANGED
@@ -137,7 +137,11 @@ class wfUpdateCheck {
137
 
138
  //Check the vulnerability database
139
  if ($slug !== null && isset($data['Version'])) {
140
- $data['vulnerable'] = $this->isPluginVulnerable($slug, $data['Version']);
 
 
 
 
141
  }
142
  else {
143
  $data['vulnerable'] = false;
@@ -175,7 +179,11 @@ class wfUpdateCheck {
175
 
176
  //Check the vulnerability database
177
  if (isset($valsArray['slug']) && isset($data['Version'])) {
178
- $data['vulnerable'] = $this->isPluginVulnerable($valsArray['slug'], $data['Version']);
 
 
 
 
179
  }
180
  else {
181
  $data['vulnerable'] = false;
@@ -396,6 +404,9 @@ class wfUpdateCheck {
396
  foreach ($vulnerableList as $r) {
397
  if ($r['slug'] == $v['slug']) {
398
  $v['vulnerable'] = !!$r['vulnerable'];
 
 
 
399
  break;
400
  }
401
  }
@@ -477,15 +488,19 @@ class wfUpdateCheck {
477
  foreach ($vulnerabilities as $v) {
478
  if ($v['slug'] == $slug) {
479
  if ($v['fromVersion'] == 'Unknown' && $v['toVersion'] == 'Unknown') {
 
480
  return $v['vulnerable'];
481
  }
482
  else if ((!isset($v['toVersion']) || $v['toVersion'] == 'Unknown') && version_compare($version, $v['fromVersion']) >= 0) {
 
483
  return $v['vulnerable'];
484
  }
485
  else if ($v['fromVersion'] == 'Unknown' && isset($v['toVersion']) && version_compare($version, $v['toVersion']) < 0) {
 
486
  return $v['vulnerable'];
487
  }
488
  else if (version_compare($version, $v['fromVersion']) >= 0 && isset($v['toVersion']) && version_compare($version, $v['toVersion']) < 0) {
 
489
  return $v['vulnerable'];
490
  }
491
  }
137
 
138
  //Check the vulnerability database
139
  if ($slug !== null && isset($data['Version'])) {
140
+ $status = $this->isPluginVulnerable($slug, $data['Version']);
141
+ $data['vulnerable'] = !!$status;
142
+ if (is_string($status)) {
143
+ $data['vulnerabilityLink'] = $status;
144
+ }
145
  }
146
  else {
147
  $data['vulnerable'] = false;
179
 
180
  //Check the vulnerability database
181
  if (isset($valsArray['slug']) && isset($data['Version'])) {
182
+ $status = $this->isPluginVulnerable($valsArray['slug'], $data['Version']);
183
+ $data['vulnerable'] = !!$status;
184
+ if (is_string($status)) {
185
+ $data['vulnerabilityLink'] = $status;
186
+ }
187
  }
188
  else {
189
  $data['vulnerable'] = false;
404
  foreach ($vulnerableList as $r) {
405
  if ($r['slug'] == $v['slug']) {
406
  $v['vulnerable'] = !!$r['vulnerable'];
407
+ if (isset($r['link'])) {
408
+ $v['link'] = $r['link'];
409
+ }
410
  break;
411
  }
412
  }
488
  foreach ($vulnerabilities as $v) {
489
  if ($v['slug'] == $slug) {
490
  if ($v['fromVersion'] == 'Unknown' && $v['toVersion'] == 'Unknown') {
491
+ if ($v['vulnerable'] && isset($v['link']) && is_string($v['link'])) { return $v['link']; }
492
  return $v['vulnerable'];
493
  }
494
  else if ((!isset($v['toVersion']) || $v['toVersion'] == 'Unknown') && version_compare($version, $v['fromVersion']) >= 0) {
495
+ if ($v['vulnerable'] && isset($v['link']) && is_string($v['link'])) { return $v['link']; }
496
  return $v['vulnerable'];
497
  }
498
  else if ($v['fromVersion'] == 'Unknown' && isset($v['toVersion']) && version_compare($version, $v['toVersion']) < 0) {
499
+ if ($v['vulnerable'] && isset($v['link']) && is_string($v['link'])) { return $v['link']; }
500
  return $v['vulnerable'];
501
  }
502
  else if (version_compare($version, $v['fromVersion']) >= 0 && isset($v['toVersion']) && version_compare($version, $v['toVersion']) < 0) {
503
+ if ($v['vulnerable'] && isset($v['link']) && is_string($v['link'])) { return $v['link']; }
504
  return $v['vulnerable'];
505
  }
506
  }
lib/wfUtils.php CHANGED
@@ -19,7 +19,7 @@ class wfUtils {
19
  if (function_exists('date_diff')) {
20
  $now = new DateTime();
21
  $utc = new DateTimeZone('UTC');
22
- $dtStr = gmdate("c", $now->getTimestamp() + $secs); //Have to do it this way because of PHP 5.2
23
  $then = new DateTime($dtStr, $utc);
24
 
25
  $diff = $then->diff($now);
@@ -103,6 +103,10 @@ class wfUtils {
103
  $components[] = self::pluralize($secs, 'second');
104
  }
105
 
 
 
 
 
106
  return implode(' ', $components);
107
  }
108
  public static function pluralize($m1, $t1, $m2 = false, $t2 = false) {
@@ -793,7 +797,7 @@ class wfUtils {
793
  return null;
794
  }
795
  $prefix = 'http';
796
- if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS']) {
797
  $prefix = 'https';
798
  }
799
  return $prefix . '://' . $host . $_SERVER['REQUEST_URI'];
@@ -1502,7 +1506,7 @@ class wfUtils {
1502
  * @param bool $inline
1503
  * @return string
1504
  */
1505
- public static function potentialBinaryStringToHTML($string, $inline = false) {
1506
  $output = '';
1507
 
1508
  if (!defined('ENT_SUBSTITUTE')) {
@@ -1521,7 +1525,7 @@ class wfUtils {
1521
  $output .= $span . '\x' . str_pad(dechex($b), 2, '0', STR_PAD_LEFT) . '</span>';
1522
  }
1523
  else if ($b < 0x80) {
1524
- $output .= htmlspecialchars($c, ENT_QUOTES, 'UTF-8');
1525
  }
1526
  else { //Assume multi-byte UTF-8
1527
  $bytes = 0;
@@ -1545,6 +1549,37 @@ class wfUtils {
1545
  }
1546
  }
1547
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1548
  if ($brokenUTF8) {
1549
  $bytes = min($bytes, strlen($string) - $i);
1550
  for ($n = 0; $n < $bytes; $n++) {
@@ -1555,7 +1590,7 @@ class wfUtils {
1555
  $i += ($bytes - 1);
1556
  }
1557
  else {
1558
- $output .= htmlspecialchars(substr($string, $i, $bytes), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
1559
  $i += ($bytes - 1);
1560
  }
1561
  }
@@ -2011,13 +2046,32 @@ class wfUtils {
2011
  *
2012
  * @return int
2013
  */
2014
- public static function normalizedTime() {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2015
  $offset = wfConfig::get('timeoffset_ntp', false);
2016
  if ($offset === false) {
2017
  $offset = wfConfig::get('timeoffset_wf', false);
2018
  if ($offset === false) { $offset = 0; }
2019
  }
2020
- return time() + $offset;
2021
  }
2022
 
2023
  /**
@@ -2033,7 +2087,7 @@ class wfUtils {
2033
  }
2034
 
2035
  $utc = new DateTimeZone('UTC');
2036
- $dtStr = gmdate("c", $timestamp); //Have to do it this way because of PHP 5.2
2037
  $dt = new DateTime($dtStr, $utc);
2038
  $tz = get_option('timezone_string');
2039
  if (!empty($tz)) {
@@ -2043,7 +2097,7 @@ class wfUtils {
2043
  $gmt = get_option('gmt_offset');
2044
  if (!empty($gmt)) {
2045
  if (PHP_VERSION_ID < 50510) {
2046
- $dtStr = gmdate("c", $timestamp + $gmt * 3600); //Have to do it this way because of < PHP 5.5.10
2047
  $dt = new DateTime($dtStr, $utc);
2048
  }
2049
  else {
19
  if (function_exists('date_diff')) {
20
  $now = new DateTime();
21
  $utc = new DateTimeZone('UTC');
22
+ $dtStr = gmdate("c", (int) ($now->getTimestamp() + $secs)); //Have to do it this way because of PHP 5.2
23
  $then = new DateTime($dtStr, $utc);
24
 
25
  $diff = $then->diff($now);
103
  $components[] = self::pluralize($secs, 'second');
104
  }
105
 
106
+ if (empty($components)) {
107
+ $components[] = 'less than 1 second';
108
+ }
109
+
110
  return implode(' ', $components);
111
  }
112
  public static function pluralize($m1, $t1, $m2 = false, $t2 = false) {
797
  return null;
798
  }
799
  $prefix = 'http';
800
+ if (is_ssl()) {
801
  $prefix = 'https';
802
  }
803
  return $prefix . '://' . $host . $_SERVER['REQUEST_URI'];
1506
  * @param bool $inline
1507
  * @return string
1508
  */
1509
+ public static function potentialBinaryStringToHTML($string, $inline = false, $allowmb4 = false) {
1510
  $output = '';
1511
 
1512
  if (!defined('ENT_SUBSTITUTE')) {
1525
  $output .= $span . '\x' . str_pad(dechex($b), 2, '0', STR_PAD_LEFT) . '</span>';
1526
  }
1527
  else if ($b < 0x80) {
1528
+ $output .= htmlspecialchars($c, ENT_QUOTES, 'ISO-8859-1');
1529
  }
1530
  else { //Assume multi-byte UTF-8
1531
  $bytes = 0;
1549
  }
1550
  }
1551
 
1552
+ if (!$brokenUTF8) { //Ensure the byte sequences are within the accepted ranges: https://tools.ietf.org/html/rfc3629
1553
+ /*
1554
+ * UTF8-octets = *( UTF8-char )
1555
+ * UTF8-char = UTF8-1 / UTF8-2 / UTF8-3 / UTF8-4
1556
+ * UTF8-1 = %x00-7F
1557
+ * UTF8-2 = %xC2-DF UTF8-tail
1558
+ * UTF8-3 = %xE0 %xA0-BF UTF8-tail / %xE1-EC 2( UTF8-tail ) /
1559
+ * %xED %x80-9F UTF8-tail / %xEE-EF 2( UTF8-tail )
1560
+ * UTF8-4 = %xF0 %x90-BF 2( UTF8-tail ) / %xF1-F3 3( UTF8-tail ) /
1561
+ * %xF4 %x80-8F 2( UTF8-tail )
1562
+ * UTF8-tail = %x80-BF
1563
+ */
1564
+
1565
+ $testString = wfUtils::substr($string, $i, $bytes);
1566
+ $regex = '/^(?:' .
1567
+ '[\xc2-\xdf][\x80-\xbf]' . //UTF8-2
1568
+ '|' . '\xe0[\xa0-\xbf][\x80-\xbf]' . //UTF8-3
1569
+ '|' . '[\xe1-\xec][\x80-\xbf]{2}' .
1570
+ '|' . '\xed[\x80-\x9f][\x80-\xbf]' .
1571
+ '|' . '[\xee-\xef][\x80-\xbf]{2}';
1572
+ if ($allowmb4) {
1573
+ $regex .= '|' . '\xf0[\x90-\xbf][\x80-\xbf]{2}' . //UTF8-4
1574
+ '|' . '[\xf1-\xf3][\x80-\xbf]{3}' .
1575
+ '|' . '\xf4[\x80-\x8f][\x80-\xbf]{2}';
1576
+ }
1577
+ $regex .= ')$/';
1578
+ if (!preg_match($regex, $testString)) {
1579
+ $brokenUTF8 = true;
1580
+ }
1581
+ }
1582
+
1583
  if ($brokenUTF8) {
1584
  $bytes = min($bytes, strlen($string) - $i);
1585
  for ($n = 0; $n < $bytes; $n++) {
1590
  $i += ($bytes - 1);
1591
  }
1592
  else {
1593
+ $output .= htmlspecialchars(wfUtils::substr($string, $i, $bytes), ENT_QUOTES | ENT_SUBSTITUTE, 'ISO-8859-1');
1594
  $i += ($bytes - 1);
1595
  }
1596
  }
2046
  *
2047
  * @return int
2048
  */
2049
+ public static function normalizedTime($base = false) {
2050
+ if ($base === false) {
2051
+ $base = time();
2052
+ }
2053
+
2054
+ $offset = wfConfig::get('timeoffset_ntp', false);
2055
+ if ($offset === false) {
2056
+ $offset = wfConfig::get('timeoffset_wf', false);
2057
+ if ($offset === false) { $offset = 0; }
2058
+ }
2059
+ return $base + $offset;
2060
+ }
2061
+
2062
+ /**
2063
+ * Returns what we consider a true timestamp, adjusted as needed to match the local server's drift. We use this
2064
+ * because a significant number of servers are using a drastically incorrect time.
2065
+ *
2066
+ * @return int
2067
+ */
2068
+ public static function denormalizedTime($base) {
2069
  $offset = wfConfig::get('timeoffset_ntp', false);
2070
  if ($offset === false) {
2071
  $offset = wfConfig::get('timeoffset_wf', false);
2072
  if ($offset === false) { $offset = 0; }
2073
  }
2074
+ return $base - $offset;
2075
  }
2076
 
2077
  /**
2087
  }
2088
 
2089
  $utc = new DateTimeZone('UTC');
2090
+ $dtStr = gmdate("c", (int) $timestamp); //Have to do it this way because of PHP 5.2
2091
  $dt = new DateTime($dtStr, $utc);
2092
  $tz = get_option('timezone_string');
2093
  if (!empty($tz)) {
2097
  $gmt = get_option('gmt_offset');
2098
  if (!empty($gmt)) {
2099
  if (PHP_VERSION_ID < 50510) {
2100
+ $dtStr = gmdate("c", (int) ($timestamp + $gmt * 3600)); //Have to do it this way because of < PHP 5.5.10
2101
  $dt = new DateTime($dtStr, $utc);
2102
  }
2103
  else {
lib/wordfenceClass.php CHANGED
@@ -1,6 +1,7 @@
1
  <?php
2
  require_once('wordfenceConstants.php');
3
  require_once('wfScanEngine.php');
 
4
  require_once('wfCrawl.php');
5
  require_once 'Diff.php';
6
  require_once 'Diff/Renderer/Html/SideBySide.php';
@@ -210,6 +211,12 @@ class wordfence {
210
  wfConfig::set('lastDashboardCheck', time());
211
  wfDashboard::processDashboardResponse($keyData['dashboard']);
212
  }
 
 
 
 
 
 
213
  }
214
  catch(Exception $e){
215
  wordfence::status(4, 'error', "Could not verify Wordfence API Key: " . $e->getMessage());
@@ -281,6 +288,11 @@ class wordfence {
281
  $report = new wfActivityReport();
282
  $report->rotateIPLog();
283
  self::_refreshUpdateNotification($report, true);
 
 
 
 
 
284
  }
285
  public static function _scheduleRefreshUpdateNotification($upgrader, $options) {
286
  $defer = false;
@@ -1016,7 +1028,7 @@ SQL
1016
  //This is messy, but not sure of a better way to do this without guaranteeing we get $wp_version
1017
  require(ABSPATH . 'wp-includes/version.php');
1018
  self::$wordfence_wp_version = $wp_version;
1019
- require('wfScan.php');
1020
  wfScan::wfScanMain();
1021
 
1022
  } //END doScan
@@ -1188,7 +1200,7 @@ SQL
1188
  self::getLog()->lockOutIP(wfUtils::getIP(), $reason);
1189
  //Then we send the email because email sending takes time and we want to block the baddie asap. If we don't users can get a lot of emails about a single attacker getting locked out.
1190
  if(wfConfig::get('alertOn_loginLockout')){
1191
- 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);
1192
  }
1193
  }
1194
  public static function isLockedOut($IP){
@@ -2592,20 +2604,20 @@ SQL
2592
 
2593
  return 'Next scan in ' . wfUtils::makeDuration($difference) . ' (' . date('M j, Y g:i:s A', $nextTime + (3600 * get_option('gmt_offset'))) . ')';
2594
  }
2595
- public static function wordfenceStartScheduledScan(){
2596
 
2597
  //If scheduled scans are not enabled in the global config option, then don't run a scheduled scan.
2598
  if(wfConfig::get('scheduledScansEnabled') != '1'){
2599
  return;
2600
  }
2601
 
2602
- //This prevents scheduled scans from piling up on low traffic blogs and all being run at once.
2603
- //Only one scheduled scan runs within a given 60 min window. Won't run if another scan has run within 30 mins.
2604
  $lastScanStart = wfConfig::get('lastScheduledScanStart', 0);
2605
- if($lastScanStart && (time() - $lastScanStart) < 1800){
2606
- //A scheduled scan was started in the last 30 mins, so skip this one.
2607
  return;
2608
  }
 
2609
  wfConfig::set('lastScheduledScanStart', time());
2610
  wordfence::status(1, 'info', "Scheduled Wordfence scan starting at " . date('l jS \of F Y h:i:s A', current_time('timestamp')) );
2611
 
@@ -2615,57 +2627,64 @@ SQL
2615
 
2616
  wfScanEngine::startScan();
2617
  }
2618
- public static function scheduleScans(){ //Idempotent. Deschedules everything and schedules the following week.
2619
  self::unscheduleAllScans();
2620
- $sched = wfConfig::get_ser('scanSched', array());
2621
- $mode = wfConfig::get('schedMode');
2622
- if($mode == 'manual' && is_array($sched) && is_array($sched[0]) ){
2623
- //Use sched as it is
2624
- } else { //Default to setting scans to run once a day at a randomly selected time.
2625
- $sched = array();
2626
- $runAt = rand(0,23);
2627
- for($day = 0; $day <= 6; $day++){
2628
- $sched[$day] = array();
2629
- for($hour = 0; $hour <= 23; $hour++){
2630
- if($hour == $runAt){
2631
- $sched[$day][$hour] = 1;
2632
- } else {
2633
- $sched[$day][$hour] = 0;
 
 
 
 
 
 
 
2634
  }
2635
  }
2636
  }
2637
  }
2638
- for($scheduledDay = 0; $scheduledDay <= 6; $scheduledDay++){
2639
- //0 is sunday
2640
- //6 is Saturday
2641
- for($scheduledHour = 0; $scheduledHour <= 23; $scheduledHour++){
2642
- if($sched[$scheduledDay][$scheduledHour]){
2643
- $wpTime = current_time('timestamp');
2644
- $currentDayOfWeek = date('w', $wpTime);
2645
- $daysInFuture = $scheduledDay - $currentDayOfWeek; //It's monday and scheduledDay is Wed (3) then result is 2 days in future. It's Wed and sched day is monday, then result is 3 - 1 = -2
2646
- if($daysInFuture < 0){ $daysInFuture += 7; } //Turns -2 into 5 days in future
2647
- $currentHour = date('G', $wpTime);
2648
- $secsOffset = ($scheduledHour - $currentHour) * 3600; //Offset from current hour, can be negative
2649
- $secondsInFuture = ($daysInFuture * 86400) + $secsOffset; //Can be negative, so we schedule those 1 week ahead
2650
- if($secondsInFuture < 1){
2651
- $secondsInFuture += (86400 * 7); //Add a week
2652
- }
2653
- $futureTime = time() - (time() % 3600) + $secondsInFuture; //Modulo rounds down to top of the hour
2654
- $futureTime += rand(0,3600); //Prevent a stampede of scans on our scanning server
2655
- wordfence::status(4, 'info', "Scheduled time for day $scheduledDay hour $scheduledHour is: " . date('l jS \of F Y h:i:s A', $futureTime));
2656
- self::scheduleSingleScan($futureTime);
2657
  }
2658
  }
2659
  }
2660
  }
2661
- public static function scheduleSingleScan($futureTime){
2662
  // Removed ability to activate on network site in v5.3.12
2663
  if (is_main_site()) {
2664
- wp_schedule_single_event($futureTime, 'wordfence_start_scheduled_scan');
 
 
 
 
 
 
 
 
2665
  }
2666
  }
2667
- private static function unscheduleAllScans(){
 
 
 
 
2668
  wp_clear_scheduled_hook('wordfence_start_scheduled_scan');
 
2669
  }
2670
  public static function ajax_saveCountryBlocking_callback(){
2671
  if(! wfConfig::get('isPaid')){
@@ -3071,6 +3090,12 @@ SQL
3071
  wfConfig::set('lastDashboardCheck', time());
3072
  wfDashboard::processDashboardResponse($keyData['dashboard']);
3073
  }
 
 
 
 
 
 
3074
  }
3075
  catch (Exception $e){
3076
  return array('errorMsg' => "Your options have been saved. However we tried to verify your API key with the Wordfence servers and received an error: " . wp_kses($e->getMessage(), array()) );
1
  <?php
2
  require_once('wordfenceConstants.php');
3
  require_once('wfScanEngine.php');
4
+ require_once('wfScan.php');
5
  require_once('wfCrawl.php');
6
  require_once 'Diff.php';
7
  require_once 'Diff/Renderer/Html/SideBySide.php';
211
  wfConfig::set('lastDashboardCheck', time());
212
  wfDashboard::processDashboardResponse($keyData['dashboard']);
213
  }
214
+ if (isset($keyData['scanSchedule']) && is_array($keyData['scanSchedule'])) {
215
+ wfConfig::set_ser('noc1ScanSchedule', $keyData['scanSchedule']);
216
+ if (!wfScan::isManualScanSchedule()) {
217
+ wordfence::scheduleScans();
218
+ }
219
+ }
220
  }
221
  catch(Exception $e){
222
  wordfence::status(4, 'error', "Could not verify Wordfence API Key: " . $e->getMessage());
288
  $report = new wfActivityReport();
289
  $report->rotateIPLog();
290
  self::_refreshUpdateNotification($report, true);
291
+
292
+ $next = self::getNextScanStartTimestamp();
293
+ if ($next - time() > 3600 && wfScan::shouldRunScan(wfScanEngine::SCAN_MODE_QUICK)) {
294
+ wfScanEngine::startScan(false, wfScanEngine::SCAN_MODE_QUICK);
295
+ }
296
  }
297
  public static function _scheduleRefreshUpdateNotification($upgrader, $options) {
298
  $defer = false;
1028
  //This is messy, but not sure of a better way to do this without guaranteeing we get $wp_version
1029
  require(ABSPATH . 'wp-includes/version.php');
1030
  self::$wordfence_wp_version = $wp_version;
1031
+ require_once('wfScan.php');
1032
  wfScan::wfScanMain();
1033
 
1034
  } //END doScan
1200
  self::getLog()->lockOutIP(wfUtils::getIP(), $reason);
1201
  //Then we send the email because email sending takes time and we want to block the baddie asap. If we don't users can get a lot of emails about a single attacker getting locked out.
1202
  if(wfConfig::get('alertOn_loginLockout')){
1203
+ wordfence::alert("User locked out from signing in", "A user with IP address {$IP} has been locked out from signing in or using the password recovery form for the following reason: {$reason}", $IP);
1204
  }
1205
  }
1206
  public static function isLockedOut($IP){
2604
 
2605
  return 'Next scan in ' . wfUtils::makeDuration($difference) . ' (' . date('M j, Y g:i:s A', $nextTime + (3600 * get_option('gmt_offset'))) . ')';
2606
  }
2607
+ public static function wordfenceStartScheduledScan($scheduledStartTime) {
2608
 
2609
  //If scheduled scans are not enabled in the global config option, then don't run a scheduled scan.
2610
  if(wfConfig::get('scheduledScansEnabled') != '1'){
2611
  return;
2612
  }
2613
 
2614
+ $minimumFrequency = (wfScan::isManualScanSchedule() ? 1800 : 43200);
 
2615
  $lastScanStart = wfConfig::get('lastScheduledScanStart', 0);
2616
+ if($lastScanStart && (time() - $lastScanStart) < $minimumFrequency){
2617
+ //A scheduled scan was started in the last 30 mins (manual schedule) or 12 hours (automatic schedule), so skip this one.
2618
  return;
2619
  }
2620
+ wfConfig::set('originalScheduledScanStart', $scheduledStartTime);
2621
  wfConfig::set('lastScheduledScanStart', time());
2622
  wordfence::status(1, 'info', "Scheduled Wordfence scan starting at " . date('l jS \of F Y h:i:s A', current_time('timestamp')) );
2623
 
2627
 
2628
  wfScanEngine::startScan();
2629
  }
2630
+ public static function scheduleScans() { //Idempotent. Deschedules everything and schedules the following week.
2631
  self::unscheduleAllScans();
2632
+ if (wfScan::isManualScanSchedule()) {
2633
+ $sched = wfConfig::get_ser('scanSched', array());
2634
+ for ($scheduledDay = 0; $scheduledDay <= 6; $scheduledDay++) {
2635
+ //0 is sunday
2636
+ //6 is Saturday
2637
+ for ($scheduledHour = 0; $scheduledHour <= 23; $scheduledHour++) {
2638
+ if ($sched[$scheduledDay][$scheduledHour]) {
2639
+ $wpTime = current_time('timestamp');
2640
+ $currentDayOfWeek = date('w', $wpTime);
2641
+ $daysInFuture = $scheduledDay - $currentDayOfWeek; //It's monday and scheduledDay is Wed (3) then result is 2 days in future. It's Wed and sched day is monday, then result is 3 - 1 = -2
2642
+ if($daysInFuture < 0){ $daysInFuture += 7; } //Turns -2 into 5 days in future
2643
+ $currentHour = date('G', $wpTime);
2644
+ $secsOffset = ($scheduledHour - $currentHour) * 3600; //Offset from current hour, can be negative
2645
+ $secondsInFuture = ($daysInFuture * 86400) + $secsOffset; //Can be negative, so we schedule those 1 week ahead
2646
+ if($secondsInFuture < 1){
2647
+ $secondsInFuture += (86400 * 7); //Add a week
2648
+ }
2649
+ $futureTime = time() - (time() % 3600) + $secondsInFuture; //Modulo rounds down to top of the hour
2650
+ $futureTime += rand(0,3600); //Prevent a stampede of scans on our scanning server
2651
+ wordfence::status(4, 'info', "Scheduled time for day $scheduledDay hour $scheduledHour is: " . date('l jS \of F Y h:i:s A', $futureTime));
2652
+ self::scheduleSingleScan($futureTime);
2653
  }
2654
  }
2655
  }
2656
  }
2657
+ else {
2658
+ $noc1ScanSchedule = wfConfig::get_ser('noc1ScanSchedule', array());
2659
+ foreach ($noc1ScanSchedule as $timestamp) {
2660
+ $timestamp = wfUtils::denormalizedTime($timestamp);
2661
+ if ($timestamp > time()) {
2662
+ self::scheduleSingleScan($timestamp);
 
 
 
 
 
 
 
 
 
 
 
 
 
2663
  }
2664
  }
2665
  }
2666
  }
2667
+ public static function scheduleSingleScan($futureTime, $originalTime = false) {
2668
  // Removed ability to activate on network site in v5.3.12
2669
  if (is_main_site()) {
2670
+ if ($originalTime === false) {
2671
+ $originalTime = $futureTime;
2672
+ }
2673
+ wp_schedule_single_event($futureTime, 'wordfence_start_scheduled_scan', array((int) $originalTime));
2674
+
2675
+ //Saving our own copy of the schedule because the wp-cron functions all require the args list to act
2676
+ $allScansScheduled = wfConfig::get_ser('allScansScheduled', array());
2677
+ $allScansScheduled[] = array('timestamp' => $futureTime, 'args' => array((int) $originalTime));
2678
+ wfConfig::set_ser('allScansScheduled', $allScansScheduled);
2679
  }
2680
  }
2681
+ private static function unscheduleAllScans() {
2682
+ $allScansScheduled = wfConfig::get_ser('allScansScheduled', array());
2683
+ foreach ($allScansScheduled as $entry) {
2684
+ wp_unschedule_event($entry['timestamp'], 'wordfence_start_scheduled_scan', $entry['args']);
2685
+ }
2686
  wp_clear_scheduled_hook('wordfence_start_scheduled_scan');
2687
+ wfConfig::set_ser('allScansScheduled', array());
2688
  }
2689
  public static function ajax_saveCountryBlocking_callback(){
2690
  if(! wfConfig::get('isPaid')){
3090
  wfConfig::set('lastDashboardCheck', time());
3091
  wfDashboard::processDashboardResponse($keyData['dashboard']);
3092
  }
3093
+ if (isset($keyData['scanSchedule']) && is_array($keyData['scanSchedule'])) {
3094
+ wfConfig::set_ser('noc1ScanSchedule', $keyData['scanSchedule']);
3095
+ if (!wfScan::isManualScanSchedule()) {
3096
+ wordfence::scheduleScans();
3097
+ }
3098
+ }
3099
  }
3100
  catch (Exception $e){
3101
  return array('errorMsg' => "Your options have been saved. However we tried to verify your API key with the Wordfence servers and received an error: " . wp_kses($e->getMessage(), array()) );
readme.txt CHANGED
@@ -3,7 +3,7 @@ Contributors: mmaunder
3
  Tags: security, secure, security plugin, wordpress security, login security, firewall, malware, antivirus, web application firewall, block hackers, country blocking
4
  Requires at least: 3.9
5
  Tested up to: 4.8.0
6
- Stable tag: 6.3.12
7
 
8
  Secure your website with the most comprehensive WordPress security plugin. Firewall, malware scan, blocking, live traffic, login security & more.
9
 
@@ -160,6 +160,17 @@ Secure your website with Wordfence.
160
 
161
  == Changelog ==
162
 
 
 
 
 
 
 
 
 
 
 
 
163
  = 6.3.12 =
164
  * Improvement: Adjusted the password audit to use a better cryptographic padding option.
165
  * Improvement: Improved the option value entry process for the modified files exclusion list.
3
  Tags: security, secure, security plugin, wordpress security, login security, firewall, malware, antivirus, web application firewall, block hackers, country blocking
4
  Requires at least: 3.9
5
  Tested up to: 4.8.0
6
+ Stable tag: 6.3.14
7
 
8
  Secure your website with the most comprehensive WordPress security plugin. Firewall, malware scan, blocking, live traffic, login security & more.
9
 
160
 
161
  == Changelog ==
162
 
163
+ = 6.3.14 =
164
+ * Improvement: Introduced smart scan distribution. Scan times are now distributed intelligently across servers to provide consistent server performance.
165
+ * Improvement: Introduced light-weight scan that runs frequently to perform checks that do not use any server resources.
166
+ * Improvement: If unable to successfully look up the status of an IP claiming to be Googlebot, the hit is now allowed.
167
+ * Improvement: Scan issue results for abandoned plugins and unpatched vulnerabilities include more info.
168
+ * Fix: Suppressed PHP notice with time formatting when a microtimestamp is passed.
169
+ * Fix: Improved binary data to HTML entity conversion to avoid wpdb stripping out-of-range UTF-8 sequences.
170
+ * Fix: Added better detection to SSL status, particularly for IIS.
171
+ * Fix: Fixed PHP notice in the diff renderer.
172
+ * Fix: Fixed typo in lockout alert.
173
+
174
  = 6.3.12 =
175
  * Improvement: Adjusted the password audit to use a better cryptographic padding option.
176
  * Improvement: Improved the option value entry process for the modified files exclusion list.
wordfence.php CHANGED
@@ -4,14 +4,14 @@ Plugin Name: Wordfence Security
4
  Plugin URI: http://www.wordfence.com/
5
  Description: Wordfence Security - Anti-virus, Firewall and Malware Scan
6
  Author: Wordfence
7
- Version: 6.3.12
8
  Author URI: http://www.wordfence.com/
9
  Network: true
10
  */
11
  if(defined('WP_INSTALLING') && WP_INSTALLING){
12
  return;
13
  }
14
- define('WORDFENCE_VERSION', '6.3.12');
15
  define('WORDFENCE_BASENAME', function_exists('plugin_basename') ? plugin_basename(__FILE__) :
16
  basename(dirname(__FILE__)) . '/' . basename(__FILE__));
17
 
4
  Plugin URI: http://www.wordfence.com/
5
  Description: Wordfence Security - Anti-virus, Firewall and Malware Scan
6
  Author: Wordfence
7
+ Version: 6.3.14
8
  Author URI: http://www.wordfence.com/
9
  Network: true
10
  */
11
  if(defined('WP_INSTALLING') && WP_INSTALLING){
12
  return;
13
  }
14
+ define('WORDFENCE_VERSION', '6.3.14');
15
  define('WORDFENCE_BASENAME', function_exists('plugin_basename') ? plugin_basename(__FILE__) :
16
  basename(dirname(__FILE__)) . '/' . basename(__FILE__));
17