PluginProbe
The WP Remote WordPress Plugin / trunk
The WP Remote WordPress Plugin vtrunk
6.72 6.69 6.65 6.62 6.48 6.47 4.87 4.97 5.05 5.09 5.16 5.22 5.24 5.25 5.38 5.41 5.42 5.45 5.47 5.53 5.56 5.65 5.68 5.72 5.73 All 53 releases
← All changes | callback/wings/security.php +315 -2 5.38trunk View file →
@@ -1,8 +1,16 @@
1 1 <?php
2 2 if (!defined('ABSPATH')) exit;
3 -if (!class_exists('BVSecurityCallback')) :
4 - class BVSecurityCallback extends BVCallbackBase {
3 +if (!class_exists('WPRSecurityCallback')) :
4 + class WPRSecurityCallback extends WPRCallbackBase {
5 + private $settings;
6 +
7 + public function __construct() {
8 + $this->settings = new WPRWPSettings();
9 + }
10 +
11 + // phpcs:disable WordPress.WP.AlternativeFunctions.file_system_operations_fread
12 + // Here we need fread as we are using popen which returns a handler
5 13 function getCrontab() {
6 14 $resp = array();
7 15
8 16 if (function_exists('exec')) {
@@ -31,13 +39,318 @@
31 39 }
32 40
33 41 return $resp;
34 42 }
43 + // phpcs:enable WordPress.WP.AlternativeFunctions.file_system_operations_fread
35 44
45 + public function setupWP2FA($secrets_by_uids, $to_encrypt, $cipher_algo, $enabled) {
46 + if (!is_array($secrets_by_uids) || !is_bool($to_encrypt) ||
47 + (!is_null($cipher_algo) && !is_string($cipher_algo)) ||
48 + (!is_null($enabled) && !is_bool($enabled))) {
49 + return array("status" => false, "message" => "Invalid parameters.");
50 + }
51 + if (count($secrets_by_uids) < 1) {
52 + return array("status" => false, "message" => "Invalid parameters.");
53 + }
54 + foreach ($secrets_by_uids as $user_id => $secret) {
55 + if (!$this->isValidUserId($user_id) || !is_string($secret)) {
56 + return array("status" => false, "message" => "Invalid parameters.");
57 + }
58 + }
59 +
60 + $result = array();
61 + $status = true;
62 + foreach ($secrets_by_uids as $user_id => $secret) {
63 + if ($to_encrypt === true) {
64 + if (empty($cipher_algo)) {
65 + $cipher_algo = WPRWP2FA::$cipher_algo;
66 + }
67 +
68 + if (defined('SECURE_AUTH_KEY')) {
69 + $encryption_result = WPRHelper::opensslEncrypt($secret, $cipher_algo, SECURE_AUTH_KEY);
70 + if ($encryption_result[0] === false) {
71 + return array("status" => false, "message" => $encryption_result[1]);
72 + }
73 + $secret = $encryption_result[1];
74 + } else {
75 + return array("status" => false, "message" => "Encryption key not found.");
76 + }
77 + }
78 +
79 + $secret_info = array(
80 + "secret" => base64_encode($secret),
81 + "is_encrypted" => $to_encrypt
82 + );
83 +
84 + $email_state_cleared = WPRWP2FAEmailOTP::revoke($user_id);
85 + $attempt_state_cleared = WPRWP2FATimeOTPLogin::clearState($user_id);
86 + $result[$user_id][WPRWP2FA::EMAIL_CHALLENGE_META_KEY] = $email_state_cleared;
87 + if (!$email_state_cleared || !$attempt_state_cleared) {
88 + $status = false;
89 + continue;
90 + }
91 +
92 + update_user_meta($user_id, WPRWP2FA::SECRET_META_KEY, $secret_info);
93 + $secret_saved = get_user_meta($user_id, WPRWP2FA::SECRET_META_KEY, true) === $secret_info;
94 + $result[$user_id][WPRWP2FA::SECRET_META_KEY] = $secret_saved;
95 + if (!$secret_saved) {
96 + $status = false;
97 + continue;
98 + }
99 +
100 + update_user_meta($user_id, WPRWP2FA::METHOD_META_KEY, 'totp');
101 + $method_saved = get_user_meta($user_id, WPRWP2FA::METHOD_META_KEY, true) === 'totp';
102 + $result[$user_id][WPRWP2FA::METHOD_META_KEY] = $method_saved;
103 + if (!$method_saved) {
104 + $status = false;
105 + continue;
106 + }
107 +
108 + update_user_meta($user_id, WPRWP2FA::FLAG_META_KEY, true);
109 + $flag_saved = get_user_meta($user_id, WPRWP2FA::FLAG_META_KEY, true) === '1';
110 + $result[$user_id][WPRWP2FA::FLAG_META_KEY] = $flag_saved;
111 + if (!$flag_saved) {
112 + $status = false;
113 + }
114 + }
115 +
116 + if (is_bool($enabled)) {
117 + $config = array("enabled" => $enabled);
118 + $this->settings->updateOption(WPRWP2FA::$wp_2fa_option, $config);
119 + $option_saved = WPRWP2FA::isEnabled($this->settings) === $enabled;
120 + $result[WPRWP2FA::$wp_2fa_option] = $option_saved;
121 + if (!$option_saved) {
122 + $status = false;
123 + }
124 + }
125 +
126 + return array("status" => $status, "result" => $result);
127 + }
128 +
129 + public function verifyWP2FACode($user_id, $code, $cipher_algo = null) {
130 + $encoded_secret_info = get_user_meta($user_id, WPRWP2FA::SECRET_META_KEY, true);
131 +
132 + $secret_info = WPRWP2FAUtils::getSecretInfo($encoded_secret_info);
133 + $secret = $secret_info['secret'];
134 + $is_secret_encrypted = $secret_info['is_encrypted'];
135 +
136 + if (is_null($secret) || is_null($is_secret_encrypted)) {
137 + return array("status" => false, "message" => "Secret and encryption status not found.");
138 + }
139 +
140 + if ($is_secret_encrypted === true) {
141 + if (empty($cipher_algo)) {
142 + $cipher_algo = WPRWP2FA::$cipher_algo;
143 + }
144 +
145 + if (defined('SECURE_AUTH_KEY')) {
146 + $decryption_result = WPRHelper::opensslDecrypt($secret, $cipher_algo, SECURE_AUTH_KEY);
147 + if ($decryption_result[0] === false) {
148 + return array("status" => false, "message" => $decryption_result[1]);
149 + }
150 + $secret = $decryption_result[1];
151 + } else {
152 + return array("status" => false, "message" => "Decryption key not found.");
153 + }
154 + }
155 +
156 + return array("status" => WPRWP2FATimeOTP::verifyCode($secret, $code, 2));
157 + }
158 +
159 + public function readWP2FAKeys($user_id) {
160 + $secret = get_user_meta($user_id, WPRWP2FA::SECRET_META_KEY, true);
161 + $enabled = get_user_meta($user_id, WPRWP2FA::FLAG_META_KEY, true);
162 + return array(
163 + "secret" => $secret,
164 + "enabled" => $enabled
165 + );
166 + }
167 +
168 + public function deleteWP2FAKeys($user_ids, $is_disable = false) {
169 + $result = array();
170 + $status = true;
171 +
172 + foreach ($user_ids as $user_id) {
173 + $secret_deleted = $this->deleteUserMetaState($user_id, WPRWP2FA::SECRET_META_KEY);
174 + $flag_deleted = $this->deleteUserMetaState($user_id, WPRWP2FA::FLAG_META_KEY);
175 + $method_deleted = $this->deleteUserMetaState($user_id, WPRWP2FA::METHOD_META_KEY);
176 + $email_state_deleted = WPRWP2FAEmailOTP::revoke($user_id);
177 + $totp_state_deleted = WPRWP2FATimeOTPLogin::clearState($user_id);
178 + $status = $status && $secret_deleted && $flag_deleted && $method_deleted &&
179 + $email_state_deleted && $totp_state_deleted;
180 + $result[$user_id] = array(
181 + WPRWP2FA::SECRET_META_KEY => $secret_deleted,
182 + WPRWP2FA::FLAG_META_KEY => $flag_deleted,
183 + WPRWP2FA::METHOD_META_KEY => $method_deleted,
184 + WPRWP2FA::EMAIL_CHALLENGE_META_KEY => $email_state_deleted
185 + );
186 + }
187 +
188 + if ($is_disable === true) {
189 + $this->settings->deleteOption(WPRWP2FA::$wp_2fa_option);
190 + $option_deleted = $this->settings->getOption(WPRWP2FA::$wp_2fa_option) === false;
191 + $result[WPRWP2FA::$wp_2fa_option] = $option_deleted;
192 + $status = $status && $option_deleted;
193 + }
194 +
195 + return array("status" => $status, "result" => $result);
196 + }
197 +
198 + private function deleteUserMetaState($user_id, $key) {
199 + delete_user_meta($user_id, $key);
200 + return !metadata_exists('user', $user_id, $key);
201 + }
202 +
203 + private function restoreEmailWP2FAMeta($user_id, $key, $value) {
204 + if ($value === '') {
205 + delete_user_meta($user_id, $key);
206 + if (get_user_meta($user_id, $key, true) !== '') update_user_meta($user_id, $key, '');
207 + } else {
208 + update_user_meta($user_id, $key, $value);
209 + }
210 + return get_user_meta($user_id, $key, true) === $value;
211 + }
212 +
213 + private function restoreEmailWP2FAUserState($user_id, $method, $flag) {
214 + $method_restored = $this->restoreEmailWP2FAMeta($user_id, WPRWP2FA::METHOD_META_KEY, $method);
215 + $flag_restored = $this->restoreEmailWP2FAMeta($user_id, WPRWP2FA::FLAG_META_KEY, $flag);
216 + return $method_restored && $flag_restored;
217 + }
218 +
219 + private function clearAuthenticatorState($user_id) {
220 + $secret_deleted = $this->deleteUserMetaState($user_id, WPRWP2FA::SECRET_META_KEY);
221 + $attempt_state_deleted = WPRWP2FATimeOTPLogin::clearState($user_id);
222 + return $secret_deleted && $attempt_state_deleted;
223 + }
224 +
225 + private function isValidUserId($user_id) {
226 + $is_integer = is_int($user_id);
227 + $is_integer_string = is_string($user_id) && ctype_digit($user_id);
228 + return ($is_integer || $is_integer_string) && intval($user_id) > 0;
229 + }
230 +
231 + public function setupEmailWP2FA($capability_version, $enabled, $targets) {
232 + if (!is_int($capability_version) || $capability_version !== 1 || $enabled !== true || !is_array($targets) || count($targets) < 1 || count($targets) > 100) return array('status' => false, 'outcomes' => array());
233 + $seen_user_ids = array();
234 + foreach ($targets as $target) {
235 + if (!is_array($target) || !isset($target['user_id']) || !is_int($target['user_id']) || $target['user_id'] < 1 || !array_key_exists('replace_existing', $target) || !is_bool($target['replace_existing']) || isset($seen_user_ids[$target['user_id']])) return array('status' => false, 'outcomes' => array());
236 + $seen_user_ids[$target['user_id']] = true;
237 + }
238 + if (!WPRWP2FAEmailOTP::hasSiteSecret()) {
239 + $outcomes = array();
240 + foreach ($targets as $target) {
241 + $outcomes[] = array('user_id' => $target['user_id'], 'status' => 'rejected', 'reason' => 'secure_secret_unavailable');
242 + }
243 + return array('status' => true, 'outcomes' => $outcomes);
244 + }
245 + $config = $this->settings->getOption(WPRWP2FA::$wp_2fa_option);
246 + if (!is_array($config)) $config = array();
247 + $config['enabled'] = true;
248 + $this->settings->updateOption(WPRWP2FA::$wp_2fa_option, $config);
249 + if (!WPRWP2FA::isEnabled($this->settings)) return array('status' => false, 'outcomes' => array());
250 + $outcomes = array();
251 + foreach ($targets as $target) {
252 + $user_id = isset($target['user_id']) ? $target['user_id'] : null;
253 + $replace = isset($target['replace_existing']) && $target['replace_existing'] === true;
254 + if (!is_int($user_id) || $user_id < 1) continue;
255 + $user = get_userdata($user_id);
256 + if (!$user) { $outcomes[] = array('user_id' => $user_id, 'status' => 'rejected', 'reason' => 'not_found'); continue; }
257 + if (!is_email($user->user_email)) { $outcomes[] = array('user_id' => $user_id, 'status' => 'rejected', 'reason' => 'invalid_email'); continue; }
258 + $current = get_user_meta($user_id, WPRWP2FA::METHOD_META_KEY, true);
259 + $has_2fa = get_user_meta($user_id, WPRWP2FA::FLAG_META_KEY, true) === '1';
260 + $current = ($has_2fa && $current === '') ? 'totp' : $current;
261 + if ($has_2fa && $current === 'email_otp') {
262 + $authenticator_state_cleared = $this->clearAuthenticatorState($user_id);
263 + $outcomes[] = array(
264 + 'user_id' => $user_id,
265 + 'status' => $authenticator_state_cleared ? 'already_configured' : 'rejected',
266 + 'reason' => $authenticator_state_cleared ? null : 'persistence_failed'
267 + );
268 + continue;
269 + }
270 + if ($has_2fa && !$replace) { $outcomes[] = array('user_id' => $user_id, 'status' => 'rejected', 'reason' => 'replacement_required'); continue; }
271 + $previous_method = get_user_meta($user_id, WPRWP2FA::METHOD_META_KEY, true);
272 + $previous_flag = get_user_meta($user_id, WPRWP2FA::FLAG_META_KEY, true);
273 + if (!WPRWP2FAEmailOTP::revoke($user_id)) {
274 + $outcomes[] = array('user_id' => $user_id, 'status' => 'rejected', 'reason' => 'persistence_failed');
275 + continue;
276 + }
277 + update_user_meta($user_id, WPRWP2FA::METHOD_META_KEY, 'email_otp');
278 + update_user_meta($user_id, WPRWP2FA::FLAG_META_KEY, true);
279 + $method_persisted = get_user_meta($user_id, WPRWP2FA::METHOD_META_KEY, true) === 'email_otp';
280 + $flag_persisted = get_user_meta($user_id, WPRWP2FA::FLAG_META_KEY, true) === '1';
281 + if (!$method_persisted || !$flag_persisted) {
282 + $rollback_restored = $this->restoreEmailWP2FAUserState($user_id, $previous_method, $previous_flag);
283 + $reason = $rollback_restored ? 'persistence_failed' : 'rollback_failed';
284 + $outcomes[] = array('user_id' => $user_id, 'status' => 'rejected', 'reason' => $reason);
285 + continue;
286 + }
287 + # Dropped only once the switch has stuck, so the rollback above still has it.
288 + if (!$this->clearAuthenticatorState($user_id)) {
289 + $outcomes[] = array('user_id' => $user_id, 'status' => 'rejected', 'reason' => 'persistence_failed');
290 + continue;
291 + }
292 + $outcomes[] = array('user_id' => $user_id, 'status' => 'configured', 'reason' => null);
293 + }
294 + return array('status' => true, 'outcomes' => $outcomes);
295 + }
296 +
36 297 public function process($request) {
298 + $params = isset($request->params) && is_array($request->params) ? $request->params : array();
299 + $invalid_params = array('status' => false, 'message' => 'Invalid parameters.');
300 +
37 301 switch ($request->method) {
38 302 case "gtcrntb":
39 303 $resp = $this->getCrontab();
304 + break;
305 + case "stupwp2fa":
306 + $secrets_by_uids = array_key_exists('secrets_by_uids', $params) ? $params['secrets_by_uids'] : null;
307 + $to_encrypt = array_key_exists('to_encrypt', $params) ? $params['to_encrypt'] : null;
308 + $cipher_algo = array_key_exists('cipher_algo', $params) ? $params['cipher_algo'] : null;
309 + $enable_wp_2fa = array_key_exists('enable_wp_2fa', $params) ? $params['enable_wp_2fa'] : null;
310 + if (!is_array($secrets_by_uids) || !is_bool($to_encrypt) ||
311 + (!is_null($cipher_algo) && !is_string($cipher_algo)) ||
312 + (!is_null($enable_wp_2fa) && !is_bool($enable_wp_2fa))) {
313 + $resp = $invalid_params;
314 + break;
315 + }
316 + $resp = $this->setupWP2FA($secrets_by_uids, $to_encrypt, $cipher_algo, $enable_wp_2fa);
317 + break;
318 + case "stupemail2fa":
319 + $capability_version = array_key_exists('capability_version', $params) ? $params['capability_version'] : null;
320 + $enable_wp_2fa = array_key_exists('enable_wp_2fa', $params) ? $params['enable_wp_2fa'] : null;
321 + $targets = array_key_exists('targets', $params) ? $params['targets'] : null;
322 + $resp = $this->setupEmailWP2FA($capability_version, $enable_wp_2fa, $targets);
323 + break;
324 + case "vrfywp2fa":
325 + $user_id = array_key_exists('user_id', $params) ? $params['user_id'] : null;
326 + $code = array_key_exists('code', $params) ? $params['code'] : null;
327 + $cipher_algo = array_key_exists('cipher_algo', $params) ? $params['cipher_algo'] : null;
328 + if (!$this->isValidUserId($user_id) || !is_string($code) ||
329 + (!is_null($cipher_algo) && !is_string($cipher_algo))) {
330 + $resp = $invalid_params;
331 + break;
332 + }
333 + $resp = $this->verifyWP2FACode($user_id, $code, $cipher_algo);
334 + break;
335 + case "rdwp2fa":
336 + $user_id = array_key_exists('user_id', $params) ? $params['user_id'] : null;
337 + $resp = $this->isValidUserId($user_id) ? $this->readWP2FAKeys($user_id) : $invalid_params;
338 + break;
339 + case "dltewp2fa":
340 + $user_ids = array_key_exists('user_ids', $params) ? $params['user_ids'] : null;
341 + $is_disable = array_key_exists('is_disable', $params) ? $params['is_disable'] : null;
342 + $valid_user_ids = is_array($user_ids);
343 + if ($valid_user_ids) {
344 + foreach ($user_ids as $user_id) {
345 + if (!$this->isValidUserId($user_id)) {
346 + $valid_user_ids = false;
347 + break;
348 + }
349 + }
350 + }
351 + $resp = ($valid_user_ids && is_bool($is_disable)) ?
352 + $this->deleteWP2FAKeys($user_ids, $is_disable) : $invalid_params;
40 353 break;
41 354 default:
42 355 $resp = false;
43 356 }