PluginProbe
Kadence Central – Site Management, Backups, Security, and Reporting / trunk
Kadence Central – Site Management, Backups, Security, and Reporting vtrunk
4.0.3 4.0.2 4.0.1 trunk 2.0.0 2.0.1 2.0.10 2.0.11 2.0.12 2.0.13 2.0.14 2.0.15 2.0.16 2.0.17 2.0.18 2.0.2 2.0.3 2.0.4 2.0.9 2.1.0 2.1.1 2.1.10 2.1.11 2.1.12 2.1.13 All 50 releases
ithemes-sync / request-handler.php

request-handler.php in Kadence Central – Site Management, Backups, Security, and Reporting trunk, at request-handler.php

575 lines 17.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /*
4 Handle requests from Sync server.
5 Written by Chris Jean for iThemes.com
6 Version 1.3.5
7
8 Version History
9 1.2.0 - 2014-01-20 - Chris Jean
10 Changed send_response() from private to public to allow for sending responses from error handlers.
11 1.2.1 - 2014-02-18 - Chris Jean
12 Added a compatibility check to ensure that Gravity Forms's updates show up and can be applied.
13 Added a function to fake that the request is taking place on an admin page. This is rudimentary and won't work for every situation.
14 1.2.2 - 2014-02-19 - Chris Jean
15 Changed method_exists to is_callable in order to avoid server-specific compatibility issues.
16 1.3.0 - 2014-02-21 - Chris Jean
17 Renamed hide_errors() to restore_error_settings() as that's a more accurate name.
18 Added hide_errors() to hide all error output.
19 hide_errors() is called when the response is done rendering to prevent stray output from messing up the server's parsing of the output.
20 1.3.1 - 2014-03-06 - Chris Jean
21 Rearranged permission-escalation code to after the request is authenticated.
22 Sync requests will now be set to emulate an Administrator user to avoid checks by some security plugins.
23 Added set_full_user_capabilities(), unset_full_user_capabilities(), and filter_user_has_cap().
24 1.3.2 - 2014-08-22 - Chris Jean
25 In order to avoid stale data, external object caches are now disabled on all authenticated requests.
26 1.3.3 - 2014-08-25 - Chris Jean
27 Disable two-factor authentication checks from the Duo Two-Factor Authentication plugin when an authenticated request is being handled.
28 1.3.4 - 2014-10-13 - Chris Jean
29 Added stronger verification of the $_POST['request'] data.
30 Obfuscated the missing-var error responses.
31 1.3.5 - 2014-11-21 - Chris Jean
32 Added smarter use of stripslashes() to avoid issues in decoding utf8 characters.
33 Moved validation check of $_POST['request'] to the constructor in order to better handle both forms of requests (legacy and admin-ajax.php).
34 */
35
36
37 require_once $GLOBALS['ithemes_sync_path'] . '/load-translations.php';
38 require_once $GLOBALS['ithemes_sync_path'] . '/functions.php';
39
40 class Ithemes_Sync_Request_Handler {
41 private $logs = [];
42 private $options = [];
43 private $old_update_data = [];
44 private $verb_time = false;
45 private $request;
46
47 private function __construct() {}
48
49 public static function for_rest_request() {
50 $self = new self();
51 $self->init_rest_handler();
52 return $self;
53 }
54
55 public static function for_legacy_request() {
56 $self = new self();
57 $self->init_legacy_handler();
58 return $self;
59 }
60
61 private function init_rest_handler() {
62 add_action( 'ithemes-sync-add-log', [ $this, 'add_log' ], 10, 2 );
63
64 $GLOBALS['ithemes_sync_request_handler'] = $this;
65
66 $this->options = $GLOBALS['ithemes-sync-settings']->get_options();
67
68 Ithemes_Sync_Functions::set_time_limit( 60 );
69
70 $this->disable_ext_object_cache();
71 }
72
73 private function init_legacy_handler() {
74 if ( empty( $_POST['request'] ) ) {
75 return;
76 }
77
78 require_once $GLOBALS['ithemes_sync_path'] . '/api.php';
79 require_once $GLOBALS['ithemes_sync_path'] . '/functions.php';
80 require_once $GLOBALS['ithemes_sync_path'] . '/settings.php';
81
82 add_action( 'ithemes-sync-add-log', [ $this, 'add_log' ], 10, 2 );
83 add_action( 'shutdown', [ $this, 'handle_error' ] );
84 add_action( 'ithemes_sync_verbs_registered', [ $this, 'handle_request' ] );
85
86 $request = $_POST['request'];
87
88 if ( ! empty( $_POST['signature'] ) ) {
89
90 // Append success and failures to response
91 $sodium_available = Ithemes_Sync_Functions::is_sodium_available();
92
93 if ( $sodium_available && ! $this->verify_request_signature( $request, $_POST['signature'] ) ) {
94 // Sodium is available and verification failed
95 do_action(
96 'ithemes-sync-add-log',
97 'signature-verification',
98 [
99 'available' => true,
100 'verified' => false,
101 ]
102 );
103
104 // $this->send_response( new WP_Error( 'request-signature-invalid', 'The request signature could not be verified' ) );
105 } elseif ( $sodium_available ) {
106 // Sodium available and signature was verified
107 do_action(
108 'ithemes-sync-add-log',
109 'signature-verification',
110 [
111 'available' => true,
112 'verified' => true,
113 ]
114 );
115 } else {
116 // Sodium is not available
117 do_action(
118 'ithemes-sync-add-log',
119 'signature-verification',
120 [
121 'available' => false,
122 'verified' => false,
123 ]
124 );
125 }
126 }
127
128 if ( ( defined( 'DOING_AJAX' ) && DOING_AJAX ) ) {
129 $request = stripslashes( $request );
130 }
131
132 $request = json_decode( $request, true );
133
134 do_action( 'solid_central_verb_request', $request );
135
136 if ( ! is_array( $request ) ) {
137 return;
138 }
139
140 $GLOBALS['ithemes_sync_request_handler'] = $this;
141
142 $this->options = $GLOBALS['ithemes-sync-settings']->get_options();
143
144 $this->parse_request( $request );
145
146 Ithemes_Sync_Functions::set_time_limit( 60 );
147
148 $this->set_current_user_to_admin();
149 $this->set_full_user_capabilities();
150 $this->disable_ext_object_cache();
151 $this->disable_2fa_verification();
152 }
153
154 private function disable_ext_object_cache() {
155 // This disables object caching that many caching plugins offer which prevents the cache from supplying stale data to Sync.
156 if ( is_callable( 'wp_using_ext_object_cache' ) ) {
157 wp_using_ext_object_cache( false );
158 }
159 }
160
161 private function disable_2fa_verification() {
162 // Disable 2FA verification of the Duo Two-Factor Authentication plugin.
163 add_filter( 'pre_site_option_duo_ikey', [ $this, 'return_empty_string' ] );
164 add_filter( 'pre_option_duo_ikey', [ $this, 'return_empty_string' ] );
165 }
166
167 private function set_current_user_to_admin() {
168 if ( ! class_exists( 'WP_Roles' ) ) {
169 do_action( 'ithemes-sync-add-log', 'The WP_Roles class does not exist. Unable to set current user to admin.' );
170 return false;
171 }
172
173 $wp_roles = new WP_Roles();
174
175 if ( ! isset( $wp_roles->roles ) ) {
176 do_action( 'ithemes-sync-add-log', 'Unable to find user roles. Unable to set current user to admin.', compact( 'wp_roles' ) );
177 return false;
178 }
179
180 $roles = $wp_roles->roles;
181
182 $max_caps = 0;
183 $power_role = false;
184
185 foreach ( $roles as $role => $role_data ) {
186 if ( ! isset( $role_data['capabilities'] ) ) {
187 continue;
188 }
189
190 $cap_count = count( $role_data['capabilities'] );
191 $new_role = false;
192
193 if ( $cap_count > $max_caps ) {
194 $power_role = $role;
195 $max_caps = $cap_count;
196 } elseif ( ( $cap_count == $max_caps ) && ( 'administrator' == $role ) ) {
197 $power_role = $role;
198 $max_caps = $cap_count;
199 }
200 }
201
202 if ( false === $power_role ) {
203 if ( isset( $roles['administrator'] ) ) {
204 $power_role = 'administrator';
205 } else {
206 $role_names = array_keys( $roles );
207 $power_role = $roles[0];
208 }
209 }
210
211 if ( false === $power_role ) {
212 do_action( 'ithemes-sync-add-log', 'Unable to find a power user role. Unable to set current user to admin.', compact( 'wp_roles' ) );
213 return false;
214 }
215
216
217 if ( ! function_exists( 'get_users' ) ) {
218 do_action( 'ithemes-sync-add-log', 'get_users() function does not exist. Unable to set current user to admin.' );
219 return false;
220 }
221
222 $users = get_users( [ 'role' => $power_role ] );
223
224 if ( ! is_array( $users ) ) {
225 do_action( 'ithemes-sync-add-log', 'get_users() retured a non-array. Unable to set current user to admin.', $users );
226 return false;
227 }
228
229 $auth_details = $GLOBALS['ithemes-sync-settings']->get_authentication_details( $this->request['user_id'] );
230 if ( $auth_details ) {
231 foreach ( $users as $u ) {
232 if ( $u->data->user_login === $auth_details['local_user'] ) {
233 // Prioritize the Sync user first, if it doesn't match for some reason, we'll fall back to any administrator user
234 $user = $u;
235 break;
236 } else {
237 $user = $u;
238 }
239 }
240 } else {
241 do_action( 'ithemes-sync-add-log', 'get_authentication_details returned false. Unable to set current user to admin.', $users );
242 return false;
243 }
244
245 if ( isset( $user->ID ) ) {
246 $GLOBALS['current_user'] = $user;
247 } else {
248 do_action( 'ithemes-sync-add-log', 'Unable to find a valid user object for the power user role. Unable to set current user to admin.', $user );
249 return false;
250 }
251
252 return true;
253 }
254
255 private function set_full_user_capabilities() {
256 add_filter( 'user_has_cap', [ $this, 'filter_user_has_cap' ], 1000, 3 );
257 }
258
259 private function unset_full_user_capabilities() {
260 remove_filter( 'user_has_cap', [ $this, 'filter_user_has_cap' ], 1000 );
261 }
262
263 public function filter_user_has_cap( $capabilities, $caps, $args ) {
264 foreach ( $caps as $cap ) {
265 $capabilities[ $cap ] = 1;
266 }
267
268 return $capabilities;
269 }
270
271 private function parse_request( $request ) {
272
273 $this->request = $request;
274
275 $required_vars = [
276 '1' => 'action',
277 '2' => 'arguments',
278 '3' => 'user_id',
279 '4' => 'hash',
280 '5' => 'salt',
281 ];
282
283 foreach ( $required_vars as $index => $var ) {
284 if ( ! isset( $request[ $var ] ) ) {
285 $this->send_response( new WP_Error( "missing-var-$index", 'Invalid request.' ) );
286 }
287 }
288
289 if ( empty( $this->options['authentications'] ) || ! isset( $this->options['authentications'][ $request['user_id'] ] ) ) {
290 $this->send_response( new WP_Error( 'user-not-authenticated', 'The requested user is not authenticated.' ) );
291 }
292
293 $user_data = $this->options['authentications'][ $request['user_id'] ];
294
295 $hash = hash( 'sha256', $request['user_id'] . $request['action'] . $this->json_encode( $request['arguments'] ) . $user_data['key'] . $request['salt'] );
296
297 if ( $hash !== $request['hash'] ) {
298 $this->send_response( new WP_Error( 'hash-mismatch', 'The hash could not be validated as a correct hash.' ) );
299 }
300 }
301
302 public function handle_request() {
303 $this->add_third_party_compatibility();
304 $this->disable_updater_transient_pre_filters();
305 $this->add_old_plugin_updater_support();
306
307 $start_time = microtime( true );
308 $results = $GLOBALS['ithemes-sync-api']->run( $this->request['action'], $this->request['arguments'] );
309 $this->verb_time = microtime( true ) - $start_time;
310
311 $this->send_response( $results );
312 }
313
314 /**
315 * Handle a WP REST verb request.
316 *
317 * @param WP_REST_Request $request The request object.
318 *
319 * @return array|WP_Error The response data or an error object.
320 */
321 public function handle_rest_request( WP_REST_Request $request ) {
322 $this->add_third_party_compatibility();
323 $this->disable_updater_transient_pre_filters();
324 $this->add_old_plugin_updater_support();
325
326 $this->request = [
327 'action' => $request['action'],
328 'arguments' => $request['arguments'],
329 ];
330
331 do_action( 'solid_central_verb_request', $this->request );
332
333 $start_time = microtime( true );
334 $results = $GLOBALS['ithemes-sync-api']->run( $this->request['action'], $this->request['arguments'] );
335 $this->verb_time = microtime( true ) - $start_time;
336 $response = [];
337
338 if ( is_wp_error( $results ) ) {
339 $response['error'] = rest_convert_error_to_response( $results )->data;
340 } else {
341 $response['response'] = $results;
342 }
343
344 if ( ! empty( $this->logs ) ) {
345 $response['logs'] = $this->logs;
346 }
347
348 $response['verb_time'] = $this->verb_time;
349
350 do_action( 'solid_central_verb_response', $response );
351
352 return $response;
353 }
354
355 public function send_response( $data ) {
356 if ( is_wp_error( $data ) ) {
357 foreach ( $data->get_error_codes() as $code ) {
358 $response['errors'][ $code ] = $data->get_error_message( $code );
359 }
360 } else {
361 $response = [
362 'response' => $data,
363 ];
364 }
365
366 if ( ! empty( $this->logs ) ) {
367 $response['logs'] = $this->logs;
368 }
369
370 $response['verb_time'] = $this->verb_time;
371
372 do_action( 'solid_central_verb_response', $response );
373
374 $json = $this->json_encode( $response );
375
376 echo "\n\nv56CHRcOT+%K\$fk[*CrQ9B5<~9T=h?xx9C</`Sqv;M{Q0ms:FR0w\n\n$json";
377
378 remove_action( 'shutdown', [ $this, 'handle_error' ] );
379
380 exit;
381 }
382
383 private function add_third_party_compatibility() {
384 if ( is_callable( [ 'RGForms', 'check_update' ] ) ) {
385 add_filter( 'transient_update_plugins', [ 'RGForms', 'check_update' ] );
386 add_filter( 'site_transient_update_plugins', [ 'RGForms', 'check_update' ] );
387 }
388 }
389
390 private function disable_updater_transient_pre_filters() {
391 // Avoid conflicts with plugins that pre-filter the update transients.
392 add_filter( 'pre_site_transient_update_plugins', [ $this, 'return_false' ], 9999 );
393 add_filter( 'pre_site_transient_update_themes', [ $this, 'return_false' ], 9999 );
394 add_filter( 'pre_site_transient_update_core', [ $this, 'return_false' ], 9999 );
395 }
396
397 public function return_false() {
398 return false;
399 }
400
401 private function add_old_plugin_updater_support() {
402 $plugins = Ithemes_Sync_Functions::get_plugin_details();
403
404 $data['3.0'] = get_site_transient( 'update_plugins' );
405 $data['2.8'] = get_transient( 'update_plugins' );
406 $data['2.6'] = get_option( 'update_plugins' );
407
408 foreach ( [ '2.8', '2.6' ] as $version ) {
409 if ( is_object( $data[ $version ] ) && ! empty( $data[ $version ]->response ) ) {
410 foreach ( $data[ $version ]->response as $plugin => $plugin_data ) {
411 if ( ! empty( $data['3.0']->response[ $plugin ] ) || ! empty( $this->old_update_data['plugins'][ $plugin ] ) ) {
412 continue;
413 }
414
415 if ( ! empty( $plugins[ $plugin ] ) && ! empty( $plugins[ $plugin ]['Version'] ) && version_compare( $plugin_data->new_version, $plugins[ $plugin ]['Version'], '<=' ) ) {
416 continue;
417 }
418
419 $this->old_update_data['plugins'][ $plugin ] = $plugin_data;
420 }
421 }
422 }
423
424 if ( empty( $this->old_update_data['plugins'] ) ) {
425 return;
426 }
427
428
429 add_filter( 'site_transient_update_plugins', [ $this, 'filter_update_plugins_add_old_update_data' ] );
430 }
431
432 public function filter_update_plugins_add_old_update_data( $update_plugins ) {
433 if ( ! isset( $update_plugins->response ) || ! is_array( $update_plugins->response ) ) {
434 return $update_plugins;
435 }
436
437 foreach ( $this->old_update_data['plugins'] as $plugin => $plugin_data ) {
438 if ( ! empty( $update_plugins->response[ $plugin ] ) ) {
439 continue;
440 }
441
442 $plugin_data->from_old_update_data = true;
443 $update_plugins->response[ $plugin ] = $plugin_data;
444 }
445
446 return $update_plugins;
447 }
448
449 public function remove_old_update_plugins_data( $plugin ) {
450 if ( empty( $this->old_update_data['plugins'] ) || ! isset( $this->old_update_data['plugins'][ $plugin ] ) ) {
451 return null;
452 }
453
454 $data['2.8'] = get_transient( 'update_plugins' );
455 $data['2.6'] = get_option( 'update_plugins' );
456
457 $found_match = [];
458
459 foreach ( [ '2.8', '2.6' ] as $version ) {
460 $found_match[ $version ] = false;
461
462 if ( is_object( $data[ $version ] ) && ! empty( $data[ $version ]->response ) && isset( $data[ $version ]->response[ $plugin ] ) ) {
463 unset( $data[ $version ]->response[ $plugin ] );
464 $found_match[ $version ] = true;
465 }
466
467 if ( empty( $data[ $version ]->response ) && ( 1 == count( get_object_vars( $data[ $version ] ) ) ) ) {
468 $data[ $version ] = false;
469 }
470 }
471
472 if ( $found_match['2.8'] ) {
473 if ( false === $data['2.8'] ) {
474 delete_transient( 'update_plugins' );
475 } else {
476 update_transient( 'update_plugins', $data['2.8'] );
477 }
478 }
479
480 if ( $found_match['2.6'] ) {
481 if ( false === $data['2.6'] ) {
482 delete_option( 'update_plugins' );
483 } else {
484 update_option( 'update_plugins', $data['2.6'] );
485 }
486 }
487
488
489 return ( $found_match['2.8'] || $found_match['2.6'] );
490 }
491
492 public function add_log( $description, $data = 'nD{k*v8}Qn4x=_7/j&r83cGD?%GWk}wb6[xal[9;y`PfpLSY[7O>b' ) {
493 if ( is_wp_error( $description ) ) {
494 $description = [
495 'type' => 'WP_Error',
496 ];
497
498 $codes = $description->get_error_codes();
499 $messages = $description->get_error_messages();
500
501 if ( 1 == count( $codes ) ) {
502 $description['code'] = current( $codes );
503 $description['message'] = current( $messages );
504 } else {
505 $description['codes'] = $codes;
506 $description['messages'] = $messages;
507 }
508 }
509
510 $log['description'] = $description;
511
512 if ( 'nD{k*v8}Qn4x=_7/j&r83cGD?%GWk}wb6[xal[9;y`PfpLSY[7O>b' != $data ) {
513 $log['data'] = $data;
514 }
515
516 do_action( 'solid_central_add_log', $log );
517
518 $this->logs[] = $log;
519 }
520
521 public function handle_error() {
522 $this->send_response( new WP_Error( 'unhandled_request', 'This request was not handled by any registered verb. This was likely caused by a fatal error.' ) );
523 }
524
525 public function return_empty_string() {
526 return '';
527 }
528
529 private function json_encode( $data ) {
530
531 $serialize_precision = ini_get( 'serialize_precision' );
532 if ( version_compare( phpversion(), '7.1', '>=' ) ) {
533 ini_set( 'serialize_precision', -1 );
534 }
535
536 $json = json_encode( $data );
537
538 if ( false === $json ) {
539 require_once $GLOBALS['ithemes_sync_path'] . '/class-ithemes-sync-json.php';
540 $json = Ithemes_Sync_JSON::encode( $data );
541 }
542
543 ini_set( 'serialize_precision', $serialize_precision );
544
545 return $json;
546 }
547
548 /**
549 * Determine if signature supplied in the request can be verified using the public key
550 *
551 * @param $request
552 * @param $signature
553 *
554 * @return bool
555 */
556 private function verify_request_signature( $request, $signature ) {
557
558 // Verify the functions we need are callable
559 if ( ! is_callable( 'sodium_base642bin' ) || ! is_callable( 'sodium_crypto_sign_verify_detached' ) ) {
560 return false;
561 }
562
563 try {
564
565 $public_key = sodium_base642bin( file_get_contents( $GLOBALS['ithemes_sync_path'] . '/public.key' ), 5 );
566 $signature = sodium_base642bin( $signature, 5 );
567
568 } catch ( Exception $e ) {
569 return false;
570 }
571
572 return sodium_crypto_sign_verify_detached( $signature, $request, $public_key );
573 }
574 }
575