Wordfence Security – Firewall & Malware Scan - Version 7.6.2

Version Description

  • September 19, 2022 =
  • Improvement: Hardened 2FA login flow to reduce exposure in cases where an attacker is able to obtain privileged information from the database
Download this release

Release Info

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

Code changes from version 7.6.1 to 7.6.2

Files changed (72) hide show
  1. css/{activity-report-widget.1662494776.css → activity-report-widget.1663593635.css} +0 -0
  2. css/{diff.1662494776.css → diff.1663593635.css} +0 -0
  3. css/{dt_table.1662494776.css → dt_table.1663593635.css} +0 -0
  4. css/{fullLog.1662494776.css → fullLog.1663593635.css} +0 -0
  5. css/{iptraf.1662494776.css → iptraf.1663593635.css} +0 -0
  6. css/{jquery-ui-timepicker-addon.1662494776.css → jquery-ui-timepicker-addon.1663593635.css} +0 -0
  7. css/{jquery-ui.min.1662494776.css → jquery-ui.min.1663593635.css} +0 -0
  8. css/{jquery-ui.structure.min.1662494776.css → jquery-ui.structure.min.1663593635.css} +0 -0
  9. css/{jquery-ui.theme.min.1662494776.css → jquery-ui.theme.min.1663593635.css} +0 -0
  10. css/license/{care-global.1662494776.css → care-global.1663593635.css} +0 -0
  11. css/license/{care.1662494776.css → care.1663593635.css} +0 -0
  12. css/license/{free-global.1662494776.css → free-global.1663593635.css} +0 -0
  13. css/license/{free.1662494776.css → free.1663593635.css} +0 -0
  14. css/license/{premium-global.1662494776.css → premium-global.1663593635.css} +0 -0
  15. css/license/{premium.1662494776.css → premium.1663593635.css} +0 -0
  16. css/license/{response-global.1662494776.css → response-global.1663593635.css} +0 -0
  17. css/license/{response-variables.1662494776.css → response-variables.1663593635.css} +0 -0
  18. css/license/{response.1662494776.css → response.1663593635.css} +0 -0
  19. css/{main.1662494776.css → main.1663593635.css} +0 -0
  20. css/{phpinfo.1662494776.css → phpinfo.1663593635.css} +0 -0
  21. css/{wf-adminbar.1662494776.css → wf-adminbar.1663593635.css} +0 -0
  22. css/{wf-colorbox.1662494776.css → wf-colorbox.1663593635.css} +0 -0
  23. css/{wf-font-awesome.1662494776.css → wf-font-awesome.1663593635.css} +0 -0
  24. css/{wf-global.1662494776.css → wf-global.1663593635.css} +0 -0
  25. css/{wf-ionicons.1662494776.css → wf-ionicons.1663593635.css} +0 -0
  26. css/{wf-onboarding.1662494776.css → wf-onboarding.1663593635.css} +0 -0
  27. css/{wf-roboto-font.1662494776.css → wf-roboto-font.1663593635.css} +0 -0
  28. css/{wfselect2.min.1662494776.css → wfselect2.min.1663593635.css} +0 -0
  29. css/{wordfenceBox.1662494776.css → wordfenceBox.1663593635.css} +0 -0
  30. js/{Chart.bundle.min.1662494776.js → Chart.bundle.min.1663593635.js} +0 -0
  31. js/{admin.1662494776.js → admin.1663593635.js} +0 -0
  32. js/{admin.ajaxWatcher.1662494776.js → admin.ajaxWatcher.1663593635.js} +0 -0
  33. js/{admin.liveTraffic.1662494776.js → admin.liveTraffic.1663593635.js} +0 -0
  34. js/{date.1662494776.js → date.1663593635.js} +0 -0
  35. js/{jquery-ui-timepicker-addon.1662494776.js → jquery-ui-timepicker-addon.1663593635.js} +0 -0
  36. js/{jquery.colorbox-min.1662494776.js → jquery.colorbox-min.1663593635.js} +0 -0
  37. js/{jquery.colorbox.1662494776.js → jquery.colorbox.1663593635.js} +0 -0
  38. js/{jquery.dataTables.min.1662494776.js → jquery.dataTables.min.1663593635.js} +0 -0
  39. js/{jquery.qrcode.min.1662494776.js → jquery.qrcode.min.1663593635.js} +0 -0
  40. js/{jquery.tmpl.min.1662494776.js → jquery.tmpl.min.1663593635.js} +0 -0
  41. js/{jquery.tools.min.1662494776.js → jquery.tools.min.1663593635.js} +0 -0
  42. js/{knockout-3.5.1.1662494776.js → knockout-3.5.1.1663593635.js} +0 -0
  43. js/{wfdashboard.1662494776.js → wfdashboard.1663593635.js} +0 -0
  44. js/{wfdropdown.1662494776.js → wfdropdown.1663593635.js} +0 -0
  45. js/{wfglobal.1662494776.js → wfglobal.1663593635.js} +0 -0
  46. js/{wfi18n.1662494776.js → wfi18n.1663593635.js} +0 -0
  47. js/{wfpopover.1662494776.js → wfpopover.1663593635.js} +0 -0
  48. js/{wfselect2.min.1662494776.js → wfselect2.min.1663593635.js} +0 -0
  49. languages/wordfence.po +2 -2
  50. modules/login-security/classes/controller/ajax.php +3 -27
  51. modules/login-security/classes/controller/totp.php +9 -5
  52. modules/login-security/classes/controller/users.php +55 -0
  53. modules/login-security/classes/controller/wordfencels.php +114 -204
  54. modules/login-security/css/{admin-global.1662494776.css → admin-global.1663593635.css} +0 -0
  55. modules/login-security/css/{admin.1662494776.css → admin.1663593635.css} +0 -0
  56. modules/login-security/css/{colorbox.1662494776.css → colorbox.1663593635.css} +0 -0
  57. modules/login-security/css/{font-awesome.1662494776.css → font-awesome.1663593635.css} +0 -0
  58. modules/login-security/css/{ionicons.1662494776.css → ionicons.1663593635.css} +0 -0
  59. modules/login-security/css/{jquery-ui.min.1662494776.css → jquery-ui.min.1663593635.css} +0 -0
  60. modules/login-security/css/{jquery-ui.structure.min.1662494776.css → jquery-ui.structure.min.1663593635.css} +0 -0
  61. modules/login-security/css/{jquery-ui.theme.min.1662494776.css → jquery-ui.theme.min.1663593635.css} +0 -0
  62. modules/login-security/css/{login.1662494776.css → login.1663593635.css} +0 -0
  63. modules/login-security/js/{admin-global.1662494776.js → admin-global.1663593635.js} +0 -0
  64. modules/login-security/js/{admin.1662494776.js → admin.1663593635.js} +0 -0
  65. modules/login-security/js/{jquery.colorbox.1662494776.js → jquery.colorbox.1663593635.js} +0 -0
  66. modules/login-security/js/{jquery.colorbox.min.1662494776.js → jquery.colorbox.min.1663593635.js} +0 -0
  67. modules/login-security/js/{jquery.qrcode.min.1662494776.js → jquery.qrcode.min.1663593635.js} +0 -0
  68. modules/login-security/js/{jquery.tmpl.min.1662494776.js → jquery.tmpl.min.1663593635.js} +0 -0
  69. modules/login-security/js/{login.1662494776.js → login.1663593635.js} +82 -59
  70. modules/login-security/wordfence-login-security.php +2 -2
  71. readme.txt +4 -1
  72. wordfence.php +3 -3
css/{activity-report-widget.1662494776.css → activity-report-widget.1663593635.css} RENAMED
File without changes
css/{diff.1662494776.css → diff.1663593635.css} RENAMED
File without changes
css/{dt_table.1662494776.css → dt_table.1663593635.css} RENAMED
File without changes
css/{fullLog.1662494776.css → fullLog.1663593635.css} RENAMED
File without changes
css/{iptraf.1662494776.css → iptraf.1663593635.css} RENAMED
File without changes
css/{jquery-ui-timepicker-addon.1662494776.css → jquery-ui-timepicker-addon.1663593635.css} RENAMED
File without changes
css/{jquery-ui.min.1662494776.css → jquery-ui.min.1663593635.css} RENAMED
File without changes
css/{jquery-ui.structure.min.1662494776.css → jquery-ui.structure.min.1663593635.css} RENAMED
File without changes
css/{jquery-ui.theme.min.1662494776.css → jquery-ui.theme.min.1663593635.css} RENAMED
File without changes
css/license/{care-global.1662494776.css → care-global.1663593635.css} RENAMED
File without changes
css/license/{care.1662494776.css → care.1663593635.css} RENAMED
File without changes
css/license/{free-global.1662494776.css → free-global.1663593635.css} RENAMED
File without changes
css/license/{free.1662494776.css → free.1663593635.css} RENAMED
File without changes
css/license/{premium-global.1662494776.css → premium-global.1663593635.css} RENAMED
File without changes
css/license/{premium.1662494776.css → premium.1663593635.css} RENAMED
File without changes
css/license/{response-global.1662494776.css → response-global.1663593635.css} RENAMED
File without changes
css/license/{response-variables.1662494776.css → response-variables.1663593635.css} RENAMED
File without changes
css/license/{response.1662494776.css → response.1663593635.css} RENAMED
File without changes
css/{main.1662494776.css → main.1663593635.css} RENAMED
File without changes
css/{phpinfo.1662494776.css → phpinfo.1663593635.css} RENAMED
File without changes
css/{wf-adminbar.1662494776.css → wf-adminbar.1663593635.css} RENAMED
File without changes
css/{wf-colorbox.1662494776.css → wf-colorbox.1663593635.css} RENAMED
File without changes
css/{wf-font-awesome.1662494776.css → wf-font-awesome.1663593635.css} RENAMED
File without changes
css/{wf-global.1662494776.css → wf-global.1663593635.css} RENAMED
File without changes
css/{wf-ionicons.1662494776.css → wf-ionicons.1663593635.css} RENAMED
File without changes
css/{wf-onboarding.1662494776.css → wf-onboarding.1663593635.css} RENAMED
File without changes
css/{wf-roboto-font.1662494776.css → wf-roboto-font.1663593635.css} RENAMED
File without changes
css/{wfselect2.min.1662494776.css → wfselect2.min.1663593635.css} RENAMED
File without changes
css/{wordfenceBox.1662494776.css → wordfenceBox.1663593635.css} RENAMED
File without changes
js/{Chart.bundle.min.1662494776.js → Chart.bundle.min.1663593635.js} RENAMED
File without changes
js/{admin.1662494776.js → admin.1663593635.js} RENAMED
File without changes
js/{admin.ajaxWatcher.1662494776.js → admin.ajaxWatcher.1663593635.js} RENAMED
File without changes
js/{admin.liveTraffic.1662494776.js → admin.liveTraffic.1663593635.js} RENAMED
File without changes
js/{date.1662494776.js → date.1663593635.js} RENAMED
File without changes
js/{jquery-ui-timepicker-addon.1662494776.js → jquery-ui-timepicker-addon.1663593635.js} RENAMED
File without changes
js/{jquery.colorbox-min.1662494776.js → jquery.colorbox-min.1663593635.js} RENAMED
File without changes
js/{jquery.colorbox.1662494776.js → jquery.colorbox.1663593635.js} RENAMED
File without changes
js/{jquery.dataTables.min.1662494776.js → jquery.dataTables.min.1663593635.js} RENAMED
File without changes
js/{jquery.qrcode.min.1662494776.js → jquery.qrcode.min.1663593635.js} RENAMED
File without changes
js/{jquery.tmpl.min.1662494776.js → jquery.tmpl.min.1663593635.js} RENAMED
File without changes
js/{jquery.tools.min.1662494776.js → jquery.tools.min.1663593635.js} RENAMED
File without changes
js/{knockout-3.5.1.1662494776.js → knockout-3.5.1.1663593635.js} RENAMED
File without changes
js/{wfdashboard.1662494776.js → wfdashboard.1663593635.js} RENAMED
File without changes
js/{wfdropdown.1662494776.js → wfdropdown.1663593635.js} RENAMED
File without changes
js/{wfglobal.1662494776.js → wfglobal.1663593635.js} RENAMED
File without changes
js/{wfi18n.1662494776.js → wfi18n.1663593635.js} RENAMED
File without changes
js/{wfpopover.1662494776.js → wfpopover.1663593635.js} RENAMED
File without changes
js/{wfselect2.min.1662494776.js → wfselect2.min.1663593635.js} RENAMED
File without changes
languages/wordfence.po CHANGED
@@ -2,14 +2,14 @@
2
  # This file is distributed under the same license as the Wordfence Security plugin.
3
  msgid ""
4
  msgstr ""
5
- "Project-Id-Version: Wordfence Security 7.6.1\n"
6
  "Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/src\n"
7
  "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
8
  "Language-Team: LANGUAGE <LL@li.org>\n"
9
  "MIME-Version: 1.0\n"
10
  "Content-Type: text/plain; charset=UTF-8\n"
11
  "Content-Transfer-Encoding: 8bit\n"
12
- "POT-Creation-Date: 2022-09-06T15:14:20-04:00\n"
13
  "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
14
  "X-Generator: WP-CLI 2.4.0\n"
15
  "X-Domain: wordfence\n"
2
  # This file is distributed under the same license as the Wordfence Security plugin.
3
  msgid ""
4
  msgstr ""
5
+ "Project-Id-Version: Wordfence Security 7.6.2\n"
6
  "Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/src\n"
7
  "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
8
  "Language-Team: LANGUAGE <LL@li.org>\n"
9
  "MIME-Version: 1.0\n"
10
  "Content-Type: text/plain; charset=UTF-8\n"
11
  "Content-Transfer-Encoding: 8bit\n"
12
+ "POT-Creation-Date: 2022-09-19T09:19:29-04:00\n"
13
  "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
14
  "X-Generator: WP-CLI 2.4.0\n"
15
  "X-Domain: wordfence\n"
modules/login-security/classes/controller/ajax.php CHANGED
@@ -196,34 +196,10 @@ class Controller_AJAX {
196
  define('WORDFENCE_LS_AUTHENTICATION_CHECK', true); //Prevents our auth filter from recursing
197
  $user = wp_authenticate($username, $password);
198
  if (is_object($user) && ($user instanceof \WP_User)) {
199
- $captcha = array();
200
- if (defined('WORDFENCE_LS_CAPTCHA_CACHE')) {
201
- $captcha = array('captcha' => WORDFENCE_LS_CAPTCHA_CACHE);
202
  }
203
-
204
- if (!Controller_Users::shared()->has_2fa_active($user) || Controller_Whitelist::shared()->is_whitelisted(Model_Request::current()->ip()) || Controller_Users::shared()->has_remembered_2fa($user)) { //Not enabled for this user, is whitelisted, or has a valid remembered cookie, pass the credentials on to the normal login flow
205
- self::send_json(array_merge($captcha, array('login' => 1)));
206
- }
207
-
208
- $encrypted = Model_Symmetric::encrypt((string) $user->ID);
209
- if (!$encrypted) { //Can't generate payload due to host failure, pass the credentials on to the normal login flow
210
- self::send_json(array_merge($captcha, array('login' => 1)));
211
- }
212
-
213
- if (defined('WORDFENCE_LS_COMBINED_IS_VALID') && WORDFENCE_LS_COMBINED_IS_VALID) {
214
- $nonce = Model_Crypto::random_bytes(32);
215
- $encrypted_nonce = Model_Symmetric::encrypt($nonce);
216
- if (!$encrypted_nonce) { //Can't generate payload due to host failure, pass the credentials on to the normal login flow
217
- self::send_json(array('login' => 1));
218
- }
219
-
220
- update_user_meta($user->ID, 'wfls-nonce', json_encode(array('nonce' => bin2hex($nonce), 'expiration' => Controller_Time::time() + 30)));
221
- $jwt = new Model_JWT(array('user' => $encrypted, 'nonce' => $encrypted_nonce), Controller_Time::time() + 30);
222
- self::send_json(array_merge($captcha, array('login' => 1, 'jwt' => (string) $jwt, 'combined' => 1)));
223
- }
224
-
225
- $jwt = new Model_JWT(array('user' => $encrypted), Controller_Time::time() + 300);
226
- self::send_json(array_merge($captcha, array('login' => 1, 'jwt' => (string) $jwt)));
227
  }
228
  else if (is_wp_error($user)) {
229
  $errors = array();
196
  define('WORDFENCE_LS_AUTHENTICATION_CHECK', true); //Prevents our auth filter from recursing
197
  $user = wp_authenticate($username, $password);
198
  if (is_object($user) && ($user instanceof \WP_User)) {
199
+ if (!Controller_Users::shared()->has_2fa_active($user) || Controller_Whitelist::shared()->is_whitelisted(Model_Request::current()->ip()) || Controller_Users::shared()->has_remembered_2fa($user) || defined('WORDFENCE_LS_COMBINED_IS_VALID')) { //Not enabled for this user, is whitelisted, has a valid remembered cookie, or has already provided a 2FA code via the password field pass the credentials on to the normal login flow
200
+ self::send_json(array('login' => 1));
 
201
  }
202
+ self::send_json(array('login' => 1, 'two_factor_required' => true));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
  }
204
  else if (is_wp_error($user)) {
205
  $errors = array();
modules/login-security/classes/controller/totp.php CHANGED
@@ -48,7 +48,7 @@ class Controller_TOTP {
48
  * @param string $code
49
  * @return bool|null Returns null if the user does not have 2FA enabled, false if the code is invalid, and true if valid.
50
  */
51
- public function validate_2fa($user, $code) {
52
  global $wpdb;
53
  $table = Controller_DB::shared()->secrets;
54
  $record = $wpdb->get_row($wpdb->prepare("SELECT * FROM `{$table}` WHERE `user_id` = %d FOR UPDATE", $user->ID), ARRAY_A);
@@ -62,9 +62,11 @@ class Controller_TOTP {
62
 
63
  $index = array_search($code, $recoveryCodes);
64
  if ($index !== false) {
65
- unset($recoveryCodes[$index]);
66
- $updatedRecoveryCodes = implode('', $recoveryCodes);
67
- $wpdb->query($wpdb->prepare("UPDATE `{$table}` SET `recovery` = X%s WHERE `id` = %d", $updatedRecoveryCodes, $record['id']));
 
 
68
  $wpdb->query('COMMIT');
69
  return true;
70
  }
@@ -75,7 +77,9 @@ class Controller_TOTP {
75
 
76
  $matches = $this->check_code($secret, $code, floor($record['vtime'] / self::TIME_WINDOW_LENGTH));
77
  if ($matches !== false) {
78
- $wpdb->query($wpdb->prepare("UPDATE `{$table}` SET `vtime` = %d WHERE `id` = %d", $matches, $record['id']));
 
 
79
  $wpdb->query('COMMIT');
80
  return true;
81
  }
48
  * @param string $code
49
  * @return bool|null Returns null if the user does not have 2FA enabled, false if the code is invalid, and true if valid.
50
  */
51
+ public function validate_2fa($user, $code, $update = true) {
52
  global $wpdb;
53
  $table = Controller_DB::shared()->secrets;
54
  $record = $wpdb->get_row($wpdb->prepare("SELECT * FROM `{$table}` WHERE `user_id` = %d FOR UPDATE", $user->ID), ARRAY_A);
62
 
63
  $index = array_search($code, $recoveryCodes);
64
  if ($index !== false) {
65
+ if ($update) {
66
+ unset($recoveryCodes[$index]);
67
+ $updatedRecoveryCodes = implode('', $recoveryCodes);
68
+ $wpdb->query($wpdb->prepare("UPDATE `{$table}` SET `recovery` = X%s WHERE `id` = %d", $updatedRecoveryCodes, $record['id']));
69
+ }
70
  $wpdb->query('COMMIT');
71
  return true;
72
  }
77
 
78
  $matches = $this->check_code($secret, $code, floor($record['vtime'] / self::TIME_WINDOW_LENGTH));
79
  if ($matches !== false) {
80
+ if ($update) {
81
+ $wpdb->query($wpdb->prepare("UPDATE `{$table}` SET `vtime` = %d WHERE `id` = %d", $matches, $record['id']));
82
+ }
83
  $wpdb->query('COMMIT');
84
  return true;
85
  }
modules/login-security/classes/controller/users.php CHANGED
@@ -12,6 +12,10 @@ class Controller_Users {
12
  const META_KEY_GRACE_PERIOD_RESET = 'wfls-grace-period-reset';
13
  const META_KEY_GRACE_PERIOD_OVERRIDE = 'wfls-grace-period-override';
14
  const META_KEY_ALLOW_GRACE_PERIOD = 'wfls-allow-grace-period';
 
 
 
 
15
 
16
  /**
17
  * Returns the singleton Controller_Users.
@@ -875,4 +879,55 @@ SQL;
875
  return $results;
876
  }
877
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
878
  }
12
  const META_KEY_GRACE_PERIOD_RESET = 'wfls-grace-period-reset';
13
  const META_KEY_GRACE_PERIOD_OVERRIDE = 'wfls-grace-period-override';
14
  const META_KEY_ALLOW_GRACE_PERIOD = 'wfls-allow-grace-period';
15
+ const META_KEY_VERIFICATION_TOKENS = 'wfls-verification-tokens';
16
+ const VERIFICATION_TOKEN_BYTES = 64;
17
+ const VERIFICATION_TOKEN_LIMIT = 5; //Max number of concurrent tokens
18
+ const VERIFICATION_TOKEN_TRANSIENT_PREFIX = 'wfls_verify_';
19
 
20
  /**
21
  * Returns the singleton Controller_Users.
879
  return $results;
880
  }
881
  }
882
+
883
+ private function get_verification_token_transient_key($hash) {
884
+ return self::VERIFICATION_TOKEN_TRANSIENT_PREFIX . $hash;
885
+ }
886
+
887
+ private function load_verification_token($hash) {
888
+ $key = $this->get_verification_token_transient_key($hash);
889
+ $userId = get_transient($key);
890
+ if ($userId === false)
891
+ return null;
892
+ return intval($userId);
893
+ }
894
+
895
+ private function load_verification_tokens($user) {
896
+ $storedHashes = get_user_meta($user->ID, self::META_KEY_VERIFICATION_TOKENS, true);
897
+ $validHashes = array();
898
+ if (is_array($storedHashes)) {
899
+ foreach ($storedHashes as $hash) {
900
+ $userId = $this->load_verification_token($hash);
901
+ if ($userId === $user->ID)
902
+ $validHashes[] = $hash;
903
+ }
904
+ }
905
+ return $validHashes;
906
+ }
907
+
908
+ private function hash_verification_token($token) {
909
+ return wp_hash($token);
910
+ }
911
+
912
+ public function generate_verification_token($user) {
913
+ $token = Model_Crypto::random_bytes(self::VERIFICATION_TOKEN_BYTES);
914
+ $hash = $this->hash_verification_token($token);
915
+ $tokens = $this->load_verification_tokens($user);
916
+ array_unshift($tokens, $hash);
917
+ while (count($tokens) > self::VERIFICATION_TOKEN_LIMIT) {
918
+ $excessHash = array_pop($tokens);
919
+ delete_transient($this->get_verification_token_transient_key($excessHash));
920
+ }
921
+ $key = $this->get_verification_token_transient_key($hash);
922
+ set_transient($key, $user->ID, WORDFENCE_LS_EMAIL_VALIDITY_DURATION_MINUTES * 60);
923
+ update_user_meta($user->ID, self::META_KEY_VERIFICATION_TOKENS, $tokens);
924
+ return base64_encode($token);
925
+ }
926
+
927
+ public function validate_verification_token($token, $user = null) {
928
+ $hash = $this->hash_verification_token(base64_decode($token));
929
+ $userId = $this->load_verification_token($hash);
930
+ return $userId !== null && ($user === null || $userId === $user->ID);
931
+ }
932
+
933
  }
modules/login-security/classes/controller/wordfencels.php CHANGED
@@ -269,13 +269,7 @@ END
269
  }
270
 
271
  if ($useCAPTCHA || Controller_Users::shared()->any_2fa_active()) {
272
- $verification = '';
273
- if (isset($_REQUEST['wfls-email-verification']) && is_string($_REQUEST['wfls-email-verification'])) {
274
- $jwt = Model_JWT::decode_jwt($_REQUEST['wfls-email-verification']);
275
- if ($jwt && isset($jwt->payload['user'])) {
276
- $verification = $_REQUEST['wfls-email-verification'];
277
- }
278
- }
279
 
280
  wp_enqueue_script('wordfence-ls-login', Model_Asset::js('login.js'), array('jquery'), WORDFENCE_LS_VERSION);
281
  wp_enqueue_style('wordfence-ls-login', Model_Asset::css('login.css'), array(), WORDFENCE_LS_VERSION);
@@ -440,6 +434,48 @@ END
440
  if (Controller_Whitelist::shared()->is_whitelisted(Model_Request::current()->ip())) { //Whitelisted, so we're not enforcing 2FA
441
  return $user;
442
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
443
 
444
  /*
445
  * CAPTCHA Check
@@ -455,7 +491,7 @@ END
455
  * below the threshold.
456
  * 5. The request is not a WooCommerce login while WC integration is disabled
457
  */
458
- if (!empty($username) && (!$this->_is_woocommerce_login() || Controller_Settings::shared()->get_bool(Controller_Settings::OPTION_ENABLE_WOOCOMMERCE_INTEGRATION))) { //Login attempt, not just a wp-login.php page load
459
 
460
  $requireCAPTCHA = Controller_CAPTCHA::shared()->is_captcha_required();
461
 
@@ -466,32 +502,12 @@ END
466
  }
467
 
468
  if ($requireCAPTCHA && !$performVerification) {
469
- if (isset($_POST['wfls-captcha-jwt']) && is_string($_POST['wfls-captcha-jwt']) && is_object($user) && $user instanceof \WP_User) {
470
- $jwt = Model_JWT::decode_jwt($_POST['wfls-captcha-jwt']);
471
- if ($jwt && isset($jwt->payload['nonce'])) {
472
- $encryptedNonce = $jwt->payload['nonce'];
473
- $nonce = Model_Symmetric::decrypt($encryptedNonce);
474
- if ($nonce) {
475
- $cachedJSON = get_user_meta($user->ID, 'wfls-captcha-nonce', true);
476
- $cached = @json_decode($cachedJSON, true); //Expected: nonce, score, token, expiration
477
- if (is_array($cached) && isset($cached['expiration']) && Controller_Time::time() <= $cached['expiration'] && hash_equals($cached['token'], $token) && hash_equals(bin2hex($nonce), $cached['nonce'])) {
478
- $score = (float) $cached['score'];
479
- }
480
- delete_user_meta($user->ID, 'wfls-captcha-nonce');
481
- }
482
- //else - unable to decrypt, probably a host error, so let it fall through to a re-check
483
- }
484
- //else - invalid JWT or host error, so let it fall through to a re-check
485
- }
486
-
487
- if (!isset($score)) {
488
- $score = Controller_CAPTCHA::shared()->score($token);
489
- if ($score === false && !Controller_CAPTCHA::shared()->test_mode()) { //An invalid token will require additional verification (if neither 2FA nor test mode are active)
490
- $performVerification = true;
491
- }
492
  }
493
  }
494
-
495
  if (!isset($score)) { $score = false; }
496
 
497
  if (is_object($user) && $user instanceof \WP_User) {
@@ -499,191 +515,78 @@ END
499
  $requireCAPTCHA = false;
500
  $performVerification = false;
501
  }
502
- else { //Cache the score/token combo for this specific user
503
- $nonce = Model_Crypto::random_bytes(32);
504
- $encryptedNonce = Model_Symmetric::encrypt($nonce);
505
- if ($encryptedNonce) {
506
- update_user_meta($user->ID, 'wfls-captcha-nonce', json_encode(array('nonce' => bin2hex($nonce), 'score' => $score, 'token' => $token, 'expiration' => Controller_Time::time() + 30)));
507
- $jwt = new Model_JWT(array('nonce' => $encryptedNonce), Controller_Time::time() + 30);
508
- if (!defined('WORDFENCE_LS_CAPTCHA_CACHE')) { define('WORDFENCE_LS_CAPTCHA_CACHE', (string) $jwt); }
509
- }
510
- // else Can't generate payload, so we'll end up re-querying the reCAPTCHA token next hit
511
- }
512
 
513
  Controller_Users::shared()->record_captcha_score($user, $score);
514
-
515
- if (isset($_REQUEST['wfls-email-verification']) && !empty($_REQUEST['wfls-email-verification']) && is_string($_REQUEST['wfls-email-verification'])) {
516
- $jwt = Model_JWT::decode_jwt($_REQUEST['wfls-email-verification']);
517
- if ($jwt && isset($jwt->payload['user'])) {
518
- $decryptedUser = Model_Symmetric::decrypt($jwt->payload['user']);
519
- if (!$decryptedUser || $decryptedUser == $user->ID) { //Skip the CAPTCHA check if the user in the JWT matches or decryption failed due to a server error
520
- $requireCAPTCHA = false;
521
- $performVerification = false;
522
- }
523
- }
524
  }
525
 
526
- if ($requireCAPTCHA && !$performVerification) {
527
- if (!Controller_CAPTCHA::shared()->is_human($score)) { //Score is below the human threshold, require email verification
528
- $performVerification = true;
529
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
530
  }
531
-
532
- if ($requireCAPTCHA && $performVerification) {
533
- $encrypted = Model_Symmetric::encrypt((string) $user->ID);
534
- if ($encrypted) {
535
- if ($this->has_woocommerce() && array_key_exists('woocommerce-login-nonce', $_POST)) {
536
- $loginUrl = get_permalink(get_option('woocommerce_myaccount_page_id'));
 
 
 
 
 
 
 
 
537
  }
538
  else {
539
- $loginUrl = wp_login_url();
540
  }
541
- $jwt = new Model_JWT(array('user' => $encrypted), Controller_Time::time() + 60 * WORDFENCE_LS_EMAIL_VALIDITY_DURATION_MINUTES);
542
- $view = new Model_View('email/login-verification', array(
543
- 'siteName' => get_bloginfo('name', 'raw'),
544
- 'siteURL' => rtrim(site_url(), '/') . '/',
545
- 'verificationURL' => add_query_arg(array('wfls-email-verification' => (string) $jwt), $loginUrl),
546
- 'ip' => Model_Request::current()->ip(),
547
- 'canEnable2FA' => Controller_Users::shared()->can_activate_2fa($user),
548
- ));
549
- wp_mail($user->user_email, __('Login Verification Required', 'wordfence-2fa'), $view->render(), "Content-Type: text/html");
550
-
551
- return new \WP_Error('wfls_captcha_verify', wp_kses(__('<strong>VERIFICATION REQUIRED</strong>: Additional verification is required for login. Please check the email address associated with the account for a verification link.', 'wordfence-2fa'), array('strong'=>array())));
552
  }
553
- //else -- Can't generate payload due to host failure, allow it to proceed
554
  }
555
- }
556
- }
557
-
558
- /*
559
- * Check 1
560
- *
561
- * If we have a valid JWT that authenticates the account _and_ code, fetch and return that user.
562
- */
563
- if (isset($_POST['wfls-token-jwt']) && is_string($_POST['wfls-token-jwt'])) {
564
- $jwt = Model_JWT::decode_jwt($_POST['wfls-token-jwt']);
565
- if (!$jwt) { //Possibly user-corrupted or expired JWT
566
- return new \WP_Error('wfls_twofactor_invalid', wp_kses(__('<strong>VALIDATION FAILED</strong>: The 2FA code could not be validated. Please try logging in again.', 'wordfence-2fa'), array('strong'=>array())));
567
- }
568
-
569
- if (!isset($jwt->payload['user'])) { //Possibly user-corrupted JWT
570
- return new \WP_Error('wfls_twofactor_invalid', wp_kses(__('<strong>VALIDATION FAILED</strong>: The 2FA code could not be validated. Please try logging in again.', 'wordfence-2fa'), array('strong'=>array())));
571
- }
572
-
573
- $decryptedUser = Model_Symmetric::decrypt($jwt->payload['user']);
574
- if (!$decryptedUser) {
575
- return $user; //Likely a server failure, allow authentication without our authenticate filter
576
- }
577
-
578
- if (isset($jwt->payload['nonce'])) { //JWT includes previous token validation
579
- $decryptedNonce = Model_Symmetric::decrypt($jwt->payload['nonce']);
580
- if (!$decryptedNonce) {
581
- return $user; //Likely a server failure, allow authentication without our authenticate filter
582
- }
583
-
584
- $expectedNonceJSON = get_user_meta((int) $decryptedUser, 'wfls-nonce', true);
585
- $expectedNonce = @json_decode($expectedNonceJSON, true);
586
- if ($expectedNonce && $expectedNonce['expiration'] > Controller_Time::time() && hash_equals($decryptedNonce, Model_Compat::hex2bin($expectedNonce['nonce']))) {
587
- delete_user_meta((int) $decryptedUser, 'wfls-nonce');
588
- $user = new \WP_User((int) $decryptedUser);
589
- return $user;
590
- }
591
-
592
- //Invalid nonce or expired nonce
593
- return new \WP_Error('wfls_twofactor_invalid', wp_kses(__('<strong>VALIDATION FAILED</strong>: The 2FA code could not be validated. Please try logging in again.', 'wordfence-2fa'), array('strong'=>array())));
594
- }
595
- }
596
-
597
- /*
598
- * Check 2
599
- *
600
- * If we don't have a valid $user at this point, it means the $username/$password combo is invalid. We'll check
601
- * to see if the user has provided a combined password in the format `<password><code>`, populating $user from
602
- * that if so.
603
- */
604
- if (!defined('WORDFENCE_LS_CHECKING_COMBINED') && (!isset($_POST['wfls-token']) || !is_string($_POST['wfls-token'])) && (!is_object($user) || !($user instanceof \WP_User))) {
605
- //Compatibility with WF legacy 2FA
606
- $combinedTOTPRegex = '/((?:[0-9]{3}\s*){2})$/i';
607
- $combinedRecoveryRegex = '/((?:[a-f0-9]{4}\s*){4})$/i';
608
- if ($this->legacy_2fa_active()) {
609
- $combinedTOTPRegex = '/(?<! wf)((?:[0-9]{3}\s*){2})$/i';
610
- $combinedRecoveryRegex = '/(?<! wf)((?:[a-f0-9]{4}\s*){4})$/i';
611
- }
612
-
613
- if (preg_match($combinedTOTPRegex, $password, $matches)) { //Possible TOTP code
614
- if (strlen($password) > strlen($matches[1])) {
615
- $revisedPassword = substr($password, 0, strlen($password) - strlen($matches[1]));
616
- $code = $matches[1];
617
- }
618
- }
619
- else if (preg_match($combinedRecoveryRegex, $password, $matches)) { //Possible recovery code
620
- if (strlen($password) > strlen($matches[1])) {
621
- $revisedPassword = substr($password, 0, strlen($password) - strlen($matches[1]));
622
- $code = $matches[1];
623
  }
624
- }
625
-
626
- if (isset($revisedPassword)) {
627
- define('WORDFENCE_LS_CHECKING_COMBINED', true); //Avoid recursing into this block
628
- if (!defined('WORDFENCE_LS_AUTHENTICATION_CHECK')) { define('WORDFENCE_LS_AUTHENTICATION_CHECK', true); }
629
- $revisedUser = wp_authenticate($username, $revisedPassword);
630
- if (is_object($revisedUser) && ($revisedUser instanceof \WP_User) && Controller_TOTP::shared()->validate_2fa($revisedUser, $code)) {
631
- define('WORDFENCE_LS_COMBINED_IS_VALID', true); //AJAX call will use this to generate a different JWT that authenticates for the account _and_ code
632
- return $revisedUser;
633
  }
634
- }
635
- }
636
-
637
- /*
638
- * Check 3
639
- *
640
- * If we have a valid JWT user and the user has provided a code, check to see if the code is valid. If it is,
641
- * the JWT user is returned.
642
- */
643
- if (isset($decryptedUser) && isset($_POST['wfls-token']) && is_string($_POST['wfls-token'])) {
644
- $jwtUser = new \WP_User((int) $decryptedUser);
645
- if (Controller_Users::shared()->has_2fa_active($jwtUser)) {
646
- if (Controller_TOTP::shared()->validate_2fa($jwtUser, $_POST['wfls-token'])) {
647
- define('WORDFENCE_LS_COMBINED_IS_VALID', true); //AJAX call will use this to generate a different JWT that authenticates for the account _and_ code
648
- return $jwtUser;
649
- }
650
-
651
- return new \WP_Error('wfls_twofactor_failed', wp_kses(__('<strong>CODE INVALID</strong>: The 2FA code provided is either expired or invalid. Please try again.', 'wordfence-2fa'), array('strong'=>array())));
652
- }
653
- }
654
-
655
- if (defined('WORDFENCE_LS_AUTHENTICATION_CHECK') && WORDFENCE_LS_AUTHENTICATION_CHECK) { //Checking for the purpose of prompting for 2FA, don't enforce it here -- AJAX calls will halt here, POST will continue
656
- return $user;
657
- }
658
-
659
- /*
660
- * Check 4
661
- *
662
- * If we have a user from a previous filter, check to see if it has 2FA enabled or a remembered 2FA. If it does, it has not
663
- * provided a code, so block its login.
664
- */
665
- if (is_object($user) && ($user instanceof \WP_User)) {
666
- if (Controller_Users::shared()->has_remembered_2fa($user)) {
667
- return $user;
668
- }
669
-
670
- $in2faGracePeriod = false;
671
- $time2faRequired = null;
672
- if (Controller_Users::shared()->has_2fa_active($user)) {
673
- $legacy2FAActive = Controller_WordfenceLS::shared()->legacy_2fa_active();
674
- if ($legacy2FAActive) {
675
- return new \WP_Error('wfls_twofactor_required', wp_kses(__('<strong>CODE REQUIRED</strong>: Please enter your 2FA code immediately after your password in the same field.', 'wordfence-2fa'), array('strong'=>array())));
676
  }
677
- return new \WP_Error('wfls_twofactor_required', wp_kses(__('<strong>CODE REQUIRED</strong>: Please provide your 2FA code when prompted.', 'wordfence-2fa'), array('strong'=>array())));
678
- }
679
- else if (Controller_Users::shared()->requires_2fa($user, $in2faGracePeriod, $time2faRequired)) {
680
- return new \WP_Error('wfls_twofactor_blocked', wp_kses(__('<strong>LOGIN BLOCKED</strong>: 2FA is required to be active on your account. Please contact the site administrator.', 'wordfence-2fa'), array('strong'=>array())));
681
- }
682
- else if ($in2faGracePeriod) {
683
- Controller_Notices::shared()->add_notice(Model_Notice::SEVERITY_CRITICAL, new Model_HTML(wp_kses(sprintf(__('You do not currently have two-factor authentication active on your account, which will be required beginning %s. <a href="%s">Configure 2FA</a>', 'wordfence-2fa'), Controller_Time::format_local_time('F j, Y g:i A', $time2faRequired), esc_url((is_multisite() && is_super_admin($user->ID)) ? network_admin_url('admin.php?page=WFLS') : admin_url('admin.php?page=WFLS'))), array('a'=>array('href'=>array())))), 'wfls-will-be-required', $user);
684
  }
 
685
  }
686
-
687
  return $user;
688
  }
689
 
@@ -731,6 +634,13 @@ END
731
  }
732
  }
733
 
 
 
 
 
 
 
 
734
  /**
735
  * @param \WP_Error $errors
736
  * @param string $redirect_to
@@ -738,9 +648,9 @@ END
738
  */
739
  public function _wp_login_errors($errors, $redirect_to) {
740
  $has_errors = (method_exists($errors, 'has_errors') ? $errors->has_errors() : !empty($errors->errors)); //has_errors was added in WP 5.1
741
- if (!$has_errors && isset($_REQUEST['wfls-email-verification']) && is_string($_REQUEST['wfls-email-verification'])) {
742
- $jwt = Model_JWT::decode_jwt($_REQUEST['wfls-email-verification']);
743
- if ($jwt && isset($jwt->payload['user'])) {
744
  $errors->add('wfls_email_verified', esc_html__('Email verification succeeded. Please continue logging in.', 'wordfence-2fa'), 'message');
745
  }
746
  else {
269
  }
270
 
271
  if ($useCAPTCHA || Controller_Users::shared()->any_2fa_active()) {
272
+ $this->validate_email_verification_token(null, $verification);
 
 
 
 
 
 
273
 
274
  wp_enqueue_script('wordfence-ls-login', Model_Asset::js('login.js'), array('jquery'), WORDFENCE_LS_VERSION);
275
  wp_enqueue_style('wordfence-ls-login', Model_Asset::css('login.css'), array(), WORDFENCE_LS_VERSION);
434
  if (Controller_Whitelist::shared()->is_whitelisted(Model_Request::current()->ip())) { //Whitelisted, so we're not enforcing 2FA
435
  return $user;
436
  }
437
+
438
+ $isLogin = !(defined('WORDFENCE_LS_AUTHENTICATION_CHECK') && WORDFENCE_LS_AUTHENTICATION_CHECK); //Checking for the purpose of prompting for 2FA, don't enforce it here
439
+ $combinedTwoFactor = false;
440
+
441
+ /*
442
+ * If we don't have a valid $user at this point, it means the $username/$password combo is invalid. We'll check
443
+ * to see if the user has provided a combined password in the format `<password><code>`, populating $user from
444
+ * that if so.
445
+ */
446
+ if (!defined('WORDFENCE_LS_CHECKING_COMBINED') && (!isset($_POST['wfls-token']) || !is_string($_POST['wfls-token'])) && (!is_object($user) || !($user instanceof \WP_User))) {
447
+ //Compatibility with WF legacy 2FA
448
+ $combinedTOTPRegex = '/((?:[0-9]{3}\s*){2})$/i';
449
+ $combinedRecoveryRegex = '/((?:[a-f0-9]{4}\s*){4})$/i';
450
+ if ($this->legacy_2fa_active()) {
451
+ $combinedTOTPRegex = '/(?<! wf)((?:[0-9]{3}\s*){2})$/i';
452
+ $combinedRecoveryRegex = '/(?<! wf)((?:[a-f0-9]{4}\s*){4})$/i';
453
+ }
454
+
455
+ if (preg_match($combinedTOTPRegex, $password, $matches)) { //Possible TOTP code
456
+ if (strlen($password) > strlen($matches[1])) {
457
+ $revisedPassword = substr($password, 0, strlen($password) - strlen($matches[1]));
458
+ $code = $matches[1];
459
+ }
460
+ }
461
+ else if (preg_match($combinedRecoveryRegex, $password, $matches)) { //Possible recovery code
462
+ if (strlen($password) > strlen($matches[1])) {
463
+ $revisedPassword = substr($password, 0, strlen($password) - strlen($matches[1]));
464
+ $code = $matches[1];
465
+ }
466
+ }
467
+
468
+ if (isset($revisedPassword)) {
469
+ define('WORDFENCE_LS_CHECKING_COMBINED', true); //Avoid recursing into this block
470
+ if (!defined('WORDFENCE_LS_AUTHENTICATION_CHECK')) { define('WORDFENCE_LS_AUTHENTICATION_CHECK', true); }
471
+ $revisedUser = wp_authenticate($username, $revisedPassword);
472
+ if (is_object($revisedUser) && ($revisedUser instanceof \WP_User) && Controller_TOTP::shared()->validate_2fa($revisedUser, $code, $isLogin)) {
473
+ define('WORDFENCE_LS_COMBINED_IS_VALID', true); //This will cause the front-end to skip the 2FA prompt
474
+ $user = $revisedUser;
475
+ $combinedTwoFactor = true;
476
+ }
477
+ }
478
+ }
479
 
480
  /*
481
  * CAPTCHA Check
491
  * below the threshold.
492
  * 5. The request is not a WooCommerce login while WC integration is disabled
493
  */
494
+ if ($isLogin && !empty($username) && (!$this->_is_woocommerce_login() || Controller_Settings::shared()->get_bool(Controller_Settings::OPTION_ENABLE_WOOCOMMERCE_INTEGRATION))) { //Login attempt, not just a wp-login.php page load
495
 
496
  $requireCAPTCHA = Controller_CAPTCHA::shared()->is_captcha_required();
497
 
502
  }
503
 
504
  if ($requireCAPTCHA && !$performVerification) {
505
+ $score = Controller_CAPTCHA::shared()->score($token);
506
+ if ($score === false && !Controller_CAPTCHA::shared()->test_mode()) { //An invalid token will require additional verification (if neither 2FA nor test mode are active)
507
+ $performVerification = true;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
508
  }
509
  }
510
+
511
  if (!isset($score)) { $score = false; }
512
 
513
  if (is_object($user) && $user instanceof \WP_User) {
515
  $requireCAPTCHA = false;
516
  $performVerification = false;
517
  }
 
 
 
 
 
 
 
 
 
 
518
 
519
  Controller_Users::shared()->record_captcha_score($user, $score);
520
+
521
+ //Skip the CAPTCHA check if the email address was verified
522
+ if ($this->validate_email_verification_token($user)) {
523
+ $requireCAPTCHA = false;
524
+ $performVerification = false;
 
 
 
 
 
525
  }
526
 
527
+ if ($requireCAPTCHA && ($performVerification || !Controller_CAPTCHA::shared()->is_human($score))) {
528
+ if ($this->has_woocommerce() && array_key_exists('woocommerce-login-nonce', $_POST)) {
529
+ $loginUrl = get_permalink(get_option('woocommerce_myaccount_page_id'));
530
  }
531
+ else {
532
+ $loginUrl = wp_login_url();
533
+ }
534
+ $verificationUrl = add_query_arg(
535
+ array(
536
+ 'wfls-email-verification' => rawurlencode(Controller_Users::shared()->generate_verification_token($user))
537
+ ),
538
+ $loginUrl
539
+ );
540
+ $view = new Model_View('email/login-verification', array(
541
+ 'siteName' => get_bloginfo('name', 'raw'),
542
+ 'siteURL' => rtrim(site_url(), '/') . '/',
543
+ 'verificationURL' => $verificationUrl,
544
+ 'ip' => Model_Request::current()->ip(),
545
+ 'canEnable2FA' => Controller_Users::shared()->can_activate_2fa($user),
546
+ ));
547
+ wp_mail($user->user_email, __('Login Verification Required', 'wordfence-2fa'), $view->render(), "Content-Type: text/html");
548
+
549
+ return new \WP_Error('wfls_captcha_verify', wp_kses(__('<strong>VERIFICATION REQUIRED</strong>: Additional verification is required for login. Please check the email address associated with the account for a verification link.', 'wordfence-2fa'), array('strong'=>array())));
550
  }
551
+
552
+ }
553
+ }
554
+
555
+ if (!$combinedTwoFactor) {
556
+
557
+ if ($isLogin && $user instanceof \WP_User) {
558
+ if (Controller_Users::shared()->has_2fa_active($user)) {
559
+ if (Controller_Users::shared()->has_remembered_2fa($user)) {
560
+ return $user;
561
+ }
562
+ elseif (array_key_exists('wfls-token', $_POST)) {
563
+ if (is_string($_POST['wfls-token']) && Controller_TOTP::shared()->validate_2fa($user, $_POST['wfls-token'])) {
564
+ return $user;
565
  }
566
  else {
567
+ return new \WP_Error('wfls_twofactor_failed', wp_kses(__('<strong>CODE INVALID</strong>: The 2FA code provided is either expired or invalid. Please try again.', 'wordfence-2fa'), array('strong'=>array())));
568
  }
 
 
 
 
 
 
 
 
 
 
 
569
  }
 
570
  }
571
+ $in2faGracePeriod = false;
572
+ $time2faRequired = null;
573
+ if (Controller_Users::shared()->has_2fa_active($user)) {
574
+ $legacy2FAActive = Controller_WordfenceLS::shared()->legacy_2fa_active();
575
+ if ($legacy2FAActive) {
576
+ return new \WP_Error('wfls_twofactor_required', wp_kses(__('<strong>CODE REQUIRED</strong>: Please enter your 2FA code immediately after your password in the same field.', 'wordfence-2fa'), array('strong'=>array())));
577
+ }
578
+ return new \WP_Error('wfls_twofactor_required', wp_kses(__('<strong>CODE REQUIRED</strong>: Please provide your 2FA code when prompted.', 'wordfence-2fa'), array('strong'=>array())));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
579
  }
580
+ else if (Controller_Users::shared()->requires_2fa($user, $in2faGracePeriod, $time2faRequired)) {
581
+ return new \WP_Error('wfls_twofactor_blocked', wp_kses(__('<strong>LOGIN BLOCKED</strong>: 2FA is required to be active on your account. Please contact the site administrator.', 'wordfence-2fa'), array('strong'=>array())));
 
 
 
 
 
 
 
582
  }
583
+ else if ($in2faGracePeriod) {
584
+ Controller_Notices::shared()->add_notice(Model_Notice::SEVERITY_CRITICAL, new Model_HTML(wp_kses(sprintf(__('You do not currently have two-factor authentication active on your account, which will be required beginning %s. <a href="%s">Configure 2FA</a>', 'wordfence-2fa'), Controller_Time::format_local_time('F j, Y g:i A', $time2faRequired), esc_url((is_multisite() && is_super_admin($user->ID)) ? network_admin_url('admin.php?page=WFLS') : admin_url('admin.php?page=WFLS'))), array('a'=>array('href'=>array())))), 'wfls-will-be-required', $user);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
585
  }
 
 
 
 
 
 
 
586
  }
587
+
588
  }
589
+
590
  return $user;
591
  }
592
 
634
  }
635
  }
636
 
637
+ private function validate_email_verification_token($user = null, &$token = null) {
638
+ $token = isset($_REQUEST['wfls-email-verification']) ? $_REQUEST['wfls-email-verification'] : null;
639
+ if (empty($token))
640
+ return null;
641
+ return is_string($token) && Controller_Users::shared()->validate_verification_token($token, $user);
642
+ }
643
+
644
  /**
645
  * @param \WP_Error $errors
646
  * @param string $redirect_to
648
  */
649
  public function _wp_login_errors($errors, $redirect_to) {
650
  $has_errors = (method_exists($errors, 'has_errors') ? $errors->has_errors() : !empty($errors->errors)); //has_errors was added in WP 5.1
651
+ $emailVerificationTokenValid = $this->validate_email_verification_token();
652
+ if (!$has_errors && $emailVerificationTokenValid !== null) {
653
+ if ($emailVerificationTokenValid) {
654
  $errors->add('wfls_email_verified', esc_html__('Email verification succeeded. Please continue logging in.', 'wordfence-2fa'), 'message');
655
  }
656
  else {
modules/login-security/css/{admin-global.1662494776.css → admin-global.1663593635.css} RENAMED
File without changes
modules/login-security/css/{admin.1662494776.css → admin.1663593635.css} RENAMED
File without changes
modules/login-security/css/{colorbox.1662494776.css → colorbox.1663593635.css} RENAMED
File without changes
modules/login-security/css/{font-awesome.1662494776.css → font-awesome.1663593635.css} RENAMED
File without changes
modules/login-security/css/{ionicons.1662494776.css → ionicons.1663593635.css} RENAMED
File without changes
modules/login-security/css/{jquery-ui.min.1662494776.css → jquery-ui.min.1663593635.css} RENAMED
File without changes
modules/login-security/css/{jquery-ui.structure.min.1662494776.css → jquery-ui.structure.min.1663593635.css} RENAMED
File without changes
modules/login-security/css/{jquery-ui.theme.min.1662494776.css → jquery-ui.theme.min.1663593635.css} RENAMED
File without changes
modules/login-security/css/{login.1662494776.css → login.1663593635.css} RENAMED
File without changes
modules/login-security/js/{admin-global.1662494776.js → admin-global.1663593635.js} RENAMED
File without changes
modules/login-security/js/{admin.1662494776.js → admin.1663593635.js} RENAMED
File without changes
modules/login-security/js/{jquery.colorbox.1662494776.js → jquery.colorbox.1663593635.js} RENAMED
File without changes
modules/login-security/js/{jquery.colorbox.min.1662494776.js → jquery.colorbox.min.1663593635.js} RENAMED
File without changes
modules/login-security/js/{jquery.qrcode.min.1662494776.js → jquery.qrcode.min.1663593635.js} RENAMED
File without changes
modules/login-security/js/{jquery.tmpl.min.1662494776.js → jquery.tmpl.min.1663593635.js} RENAMED
File without changes
modules/login-security/js/{login.1662494776.js → login.1663593635.js} RENAMED
@@ -259,8 +259,64 @@
259
  }
260
  });
261
  };
262
-
263
- var wfls_query_ajax = function() {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
264
  $('.wfls-login-message').remove();
265
 
266
  if (!loginLocator.locate()) {
@@ -280,9 +336,8 @@
280
  dataType: 'json',
281
  data: data,
282
  success: function(json) {
283
- form.data('wflsLoggingIn', 0);
284
  if (json.hasOwnProperty('reset') && json.reset) {
285
- $('#wfls-prompt-overlay, #wfls-token-jwt').remove();
286
  }
287
 
288
  if (json.hasOwnProperty('error')) {
@@ -316,27 +371,10 @@
316
  wfls_init_captcha();
317
  wfls_init_captcha_contact();
318
  }
319
-
320
- if (json.hasOwnProperty('jwt')) {
321
- var jwtField = $('#wfls-token-jwt');
322
- if (!jwtField.length) {
323
- jwtField = $('<input type="hidden" name="wfls-token-jwt" id="wfls-token-jwt" value=""/>');
324
- form.append(jwtField);
325
- }
326
- $('#wfls-token-jwt').val(json.jwt);
327
-
328
- if (parseInt(WFLSVars.useCAPTCHA)) {
329
- wfls_init_captcha();
330
- wfls_init_captcha_contact();
331
- }
332
-
333
- if (json.hasOwnProperty('combined')) {
334
- form.data('wflsLoggingIn', 1);
335
- $('#wp-submit,[type=submit][name=login]').trigger('click');
336
- return;
337
- }
338
 
339
- if (!$('#wfls-token').length) {
 
 
340
  var overlay = $('<div id="wfls-prompt-overlay"></div>');
341
  var wrapper = $('<div id="wfls-prompt-wrapper"></div>');
342
  var label = $('<p><label for="wfls-token">2FA Code <a href="javascript:void(0)" class="wfls-2fa-code-help wfls-tooltip-trigger" title="The 2FA Code can be found within the authenticator app you used when first activating two-factor authentication. You may also use one of your recovery codes."><i class="dashicons dashicons-editor-help"></i></a></label></p>');
@@ -351,24 +389,16 @@
351
  wrapper.append(button);
352
  overlay.append(wrapper);
353
  form.css('position', 'relative').append(overlay);
354
-
355
- new $.Zebra_Tooltips($('.wfls-tooltip-trigger'));
356
-
357
- $('#wfls-token-submit').on('click', function(e) {
358
- e.preventDefault();
359
- e.stopPropagation();
360
 
361
- wfls_query_ajax();
362
- });
363
  }
364
-
365
- $('#wfls-token').focus();
366
  }
367
  else { //Unexpected response, skip AJAX and process via the regular login flow
368
- form.data('wflsLoggingIn', 1);
369
- $('#wp-submit,[type=submit][name=login]').trigger('click');
370
  }
371
  }
 
372
  },
373
  error: function(err) {
374
  if (err.status == 503 || err.status == 403) {
@@ -376,6 +406,7 @@
376
  return;
377
  }
378
  showLoginMessage('<strong>ERROR</strong>: An error was encountered while trying to authenticate. Please try again.', 'error');
 
379
  }
380
  });
381
  };
@@ -383,42 +414,34 @@
383
  $(function() {
384
  //Login
385
  if (loginLocator.locate()) {
386
- loginLocator.getForm().on('submit', function(e) {
387
- var loggingIn = !!parseInt($(this).data('wflsLoggingIn'));
388
- $(this).data('wflsLoggingIn', 0);
389
- if (loggingIn) { return; }
390
-
391
- e.preventDefault();
392
- e.stopPropagation();
393
-
394
  if (parseInt(WFLSVars.useCAPTCHA)) {
395
- wfls_init_captcha(function() { wfls_query_ajax(); });
396
  }
397
  else {
398
- wfls_query_ajax();
399
  }
400
  });
401
  }
402
 
403
  //Registration
404
- if (registrationLocator.locate()) {
405
- registrationLocator.getForm().on('submit', function(e) {
406
- var form = $(this);
407
- var registering = !!parseInt(form.data('wflsRegistering'));
408
- form.data('wflsRegistering', 0);
409
- if (!registering && parseInt(WFLSVars.useCAPTCHA)) {
410
- e.preventDefault();
411
- e.stopPropagation();
412
-
413
- form.data('wflsRegistering', 1);
414
- wfls_init_captcha(function() { form.find('[type=submit]').first().trigger('click'); }, registrationLocator.getInput());
415
- }
416
  });
417
  }
418
 
419
  var verificationField = $('#wfls-email-verification');
420
  if (verificationField.length) {
421
- verificationField.val(WFLSVars.verification);
422
  }
423
  else {
424
  var log = getRelevantInputs();
259
  }
260
  });
261
  };
262
+
263
+ function FormBlocker(form, buttonSelector, clickOnSubmit) {
264
+
265
+ var self = this;
266
+ var blocked = false;
267
+ var released = false;
268
+ clickOnSubmit = clickOnSubmit || false;
269
+ var clickSubmitInProgress = false;
270
+
271
+ this.getButtons = function() {
272
+ return form.find(buttonSelector);
273
+ }
274
+
275
+ this.block = function() {
276
+ if (blocked)
277
+ return false;
278
+ blocked = true;
279
+ this.getButtons().addClass('disabled').prop('disabled', true);
280
+ return true;
281
+ }
282
+
283
+ this.unblock = function() {
284
+ this.getButtons().removeClass('disabled').prop('disabled', false);
285
+ blocked = false;
286
+ }
287
+
288
+ this.release = function() {
289
+ this.released = true;
290
+ }
291
+
292
+ this.clickSubmit = function() {
293
+ this.unblock();
294
+ this.getButtons().first().trigger('click');
295
+ }
296
+
297
+ this.initialize = function(callback) {
298
+ form.on('submit', function(event) {
299
+ if (self.released && (!self.clickOnSubmit || self.clickSubmitInProgress)) {
300
+ if (self.clickSubmitInProgress)
301
+ self.clickSubmitInProgress = false;
302
+ return;
303
+ }
304
+ event.preventDefault();
305
+ event.stopPropagation();
306
+ if (self.released) {
307
+ self.clickSubmitInProgress = true;
308
+ self.clickSubmit();
309
+ return;
310
+ }
311
+ if (self.block()) {
312
+ callback();
313
+ }
314
+ });
315
+ }
316
+
317
+ }
318
+
319
+ var wfls_query_ajax = function(blocker) {
320
  $('.wfls-login-message').remove();
321
 
322
  if (!loginLocator.locate()) {
336
  dataType: 'json',
337
  data: data,
338
  success: function(json) {
 
339
  if (json.hasOwnProperty('reset') && json.reset) {
340
+ $('#wfls-prompt-overlay').remove();
341
  }
342
 
343
  if (json.hasOwnProperty('error')) {
371
  wfls_init_captcha();
372
  wfls_init_captcha_contact();
373
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
374
 
375
+ blocker.release();
376
+ if (json.hasOwnProperty('two_factor_required') && json.two_factor_required) {
377
+ if ($('#wfls-prompt-overlay').length === 0) {
378
  var overlay = $('<div id="wfls-prompt-overlay"></div>');
379
  var wrapper = $('<div id="wfls-prompt-wrapper"></div>');
380
  var label = $('<p><label for="wfls-token">2FA Code <a href="javascript:void(0)" class="wfls-2fa-code-help wfls-tooltip-trigger" title="The 2FA Code can be found within the authenticator app you used when first activating two-factor authentication. You may also use one of your recovery codes."><i class="dashicons dashicons-editor-help"></i></a></label></p>');
389
  wrapper.append(button);
390
  overlay.append(wrapper);
391
  form.css('position', 'relative').append(overlay);
392
+ $('#wfls-token').focus();
 
 
 
 
 
393
 
394
+ new $.Zebra_Tooltips($('.wfls-tooltip-trigger'));
 
395
  }
 
 
396
  }
397
  else { //Unexpected response, skip AJAX and process via the regular login flow
398
+ blocker.clickSubmit();
 
399
  }
400
  }
401
+ blocker.unblock();
402
  },
403
  error: function(err) {
404
  if (err.status == 503 || err.status == 403) {
406
  return;
407
  }
408
  showLoginMessage('<strong>ERROR</strong>: An error was encountered while trying to authenticate. Please try again.', 'error');
409
+ blocker.unblock();
410
  }
411
  });
412
  };
414
  $(function() {
415
  //Login
416
  if (loginLocator.locate()) {
417
+ var loginBlocker = new FormBlocker(loginLocator.getForm(), '#wp-submit,[type=submit][name=login]', true);
418
+ loginBlocker.initialize(function() {
 
 
 
 
 
 
419
  if (parseInt(WFLSVars.useCAPTCHA)) {
420
+ wfls_init_captcha(function() { wfls_query_ajax(loginBlocker); });
421
  }
422
  else {
423
+ wfls_query_ajax(loginBlocker);
424
  }
425
  });
426
  }
427
 
428
  //Registration
429
+ if (registrationLocator.locate() && parseInt(WFLSVars.useCAPTCHA)) {
430
+ var registrationBlocker = new FormBlocker(registrationLocator.getForm(), '[type=submit]');
431
+ registrationBlocker.initialize(function() {
432
+ wfls_init_captcha(
433
+ function() {
434
+ registrationBlocker.release();
435
+ registrationBlocker.clickSubmit();
436
+ },
437
+ registrationLocator.getInput()
438
+ );
 
 
439
  });
440
  }
441
 
442
  var verificationField = $('#wfls-email-verification');
443
  if (verificationField.length) {
444
+ verificationField.val(WFLSVars.verification || '');
445
  }
446
  else {
447
  var log = getRelevantInputs();
modules/login-security/wordfence-login-security.php CHANGED
@@ -26,8 +26,8 @@ if ($wfCoreActive && !(isset($wfCoreLoading) && $wfCoreLoading)) {
26
  else {
27
  define('WORDFENCE_LS_FROM_CORE', ($wfCoreActive && isset($wfCoreLoading) && $wfCoreLoading));
28
 
29
- define('WORDFENCE_LS_VERSION', '1.0.10');
30
- define('WORDFENCE_LS_BUILD_NUMBER', '1662494776');
31
 
32
  define('WORDFENCE_LS_PLUGIN_BASENAME', plugin_basename(__FILE__));
33
 
26
  else {
27
  define('WORDFENCE_LS_FROM_CORE', ($wfCoreActive && isset($wfCoreLoading) && $wfCoreLoading));
28
 
29
+ define('WORDFENCE_LS_VERSION', '1.0.11-rc3');
30
+ define('WORDFENCE_LS_BUILD_NUMBER', '1663593635');
31
 
32
  define('WORDFENCE_LS_PLUGIN_BASENAME', plugin_basename(__FILE__));
33
 
readme.txt CHANGED
@@ -4,7 +4,7 @@ Tags: security, firewall, malware scanner, web application firewall, two factor
4
  Requires at least: 3.9
5
  Requires PHP: 5.3
6
  Tested up to: 6.0
7
- Stable tag: 7.6.1
8
  License: GPLv3
9
  License URI: https://www.gnu.org/licenses/gpl-3.0.html
10
 
@@ -185,6 +185,9 @@ Secure your website with Wordfence.
185
 
186
  == Changelog ==
187
 
 
 
 
188
  = 7.6.1 - September 6, 2022 =
189
  * Fix: Prevented XSS that would have required admin privileges to exploit (CVE-2022-3144)
190
 
4
  Requires at least: 3.9
5
  Requires PHP: 5.3
6
  Tested up to: 6.0
7
+ Stable tag: 7.6.2
8
  License: GPLv3
9
  License URI: https://www.gnu.org/licenses/gpl-3.0.html
10
 
185
 
186
  == Changelog ==
187
 
188
+ = 7.6.2 - September 19, 2022 =
189
+ * Improvement: Hardened 2FA login flow to reduce exposure in cases where an attacker is able to obtain privileged information from the database
190
+
191
  = 7.6.1 - September 6, 2022 =
192
  * Fix: Prevented XSS that would have required admin privileges to exploit (CVE-2022-3144)
193
 
wordfence.php CHANGED
@@ -4,7 +4,7 @@ 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: 7.6.1
8
  Author URI: http://www.wordfence.com/
9
  Text Domain: wordfence
10
  Domain Path: /languages
@@ -38,8 +38,8 @@ if(defined('WP_INSTALLING') && WP_INSTALLING){
38
  if (!defined('ABSPATH')) {
39
  exit;
40
  }
41
- define('WORDFENCE_VERSION', '7.6.1');
42
- define('WORDFENCE_BUILD_NUMBER', '1662494776');
43
  define('WORDFENCE_BASENAME', function_exists('plugin_basename') ? plugin_basename(__FILE__) :
44
  basename(dirname(__FILE__)) . '/' . basename(__FILE__));
45
 
4
  Plugin URI: http://www.wordfence.com/
5
  Description: Wordfence Security - Anti-virus, Firewall and Malware Scan
6
  Author: Wordfence
7
+ Version: 7.6.2
8
  Author URI: http://www.wordfence.com/
9
  Text Domain: wordfence
10
  Domain Path: /languages
38
  if (!defined('ABSPATH')) {
39
  exit;
40
  }
41
+ define('WORDFENCE_VERSION', '7.6.2');
42
+ define('WORDFENCE_BUILD_NUMBER', '1663593635');
43
  define('WORDFENCE_BASENAME', function_exists('plugin_basename') ? plugin_basename(__FILE__) :
44
  basename(dirname(__FILE__)) . '/' . basename(__FILE__));
45