PluginProbe
MainWP Dashboard: Self-hosted WordPress Management for Agencies / 6.2.1
MainWP Dashboard: Self-hosted WordPress Management for Agencies v6.2.1
6.2.2 6.2.1 6.2 6.1.8 6.1.7 6.1.6 6.1.5 6.1.4 6.1.3 6.1.2 6.1.1 6.1 6.0.12 6.0.11 4.6.0.1 5.0 5.0.1 5.0.2 5.0.3 5.0.3.1 5.0.3.2 5.1 5.1.1 5.2 5.2.1 All 155 releases
← All changes | includes/rest-api/controller/version1/class-mainwp-rest-api-v1.php +112 -25 5.2 → 6.2.1 View file →
@@ -24,8 +24,13 @@
24 24 use MainWP\Dashboard\MainWP_Error_Helper;
25 25 use MainWP\Dashboard\MainWP_Exception;
26 26 use Exception;
27 27
28 +// Exit if accessed directly.
29 +if ( ! defined( 'ABSPATH' ) ) {
30 + exit;
31 +}
32 +
28 33 /**
29 34 * Class Rest_Api_V1
30 35 *
31 36 * @package MainWP\Dashboard
@@ -76,8 +81,15 @@
76 81 * Adds an action to generate API credentials.
77 82 * Adds an action to create the rest API endpoints if activated in the plugin settings.
78 83 */
79 84 public function init() {
85 + add_action( 'init', array( &$this, 'init_hooks' ), 9 ); // Fix the issue where database tables are not created on a fresh install.
86 + }
87 +
88 + /**
89 + * Method init hooks.
90 + */
91 + public function init_hooks() {
80 92 if ( $this->is_rest_api_enabled() ) {
81 93 add_filter( 'mainwp_rest_api_validate', array( &$this, 'rest_api_validate' ), 10, 2 ); // to compatible version 1 rest api validate.
82 94 // run API.
83 95 if ( $this->enabled_rest_v1_api() ) {
@@ -111,22 +123,9 @@
111 123 *
112 124 * Check if enable the REST API.
113 125 */
114 126 private function enabled_rest_v1_api() {
115 -
116 - $all_keys = get_option( 'mainwp_rest_api_keys', false );
117 -
118 - if ( ! is_array( $all_keys ) ) {
119 - return false;
120 - }
121 -
122 - foreach ( $all_keys as $item ) {
123 - if ( ! empty( $item['cs'] ) && ! empty( $item['enabled'] ) ) {
124 - return true; // one key enabled, enabled the REST API.
125 - }
126 - }
127 -
128 - return false; // all keys disabled.
127 + return get_option( 'mainwp_rest_api_keys', true ); // enabled the REST API if there is any key.
129 128 }
130 129
131 130 /**
132 131 * Method enabled_rest_v2_api()
@@ -133,9 +132,9 @@
133 132 *
134 133 * Enabled the REST API.
135 134 */
136 135 private function enabled_rest_v2_api() {
137 - return MainWP_DB::instance()->is_existed_enabled_rest_key();
136 + return true; // enabled the REST API if there is any key.
138 137 }
139 138
140 139 /**
141 140 * Method hook_rest_api_enabled()
@@ -518,8 +517,79 @@
518 517 }
519 518 }
520 519
521 520 /**
521 + * Sensitive site fields that must never appear in v1 REST responses.
522 + *
523 + * The data layer projection at MainWP_DB::get_websites_for_current_user
524 + * returns these when full_data=true so that internal callers (cron jobs,
525 + * MainWP_Connect) can sign requests to child sites. v1 REST callbacks
526 + * pass through that projection directly, which is the leak path
527 + * MWP-1542 addresses. Centralizing the strip list here keeps every
528 + * callback in sync; copying the safe template at mainwp_rest_api_site_callback
529 + * previously inherited only ['privkey', 'adminname'] which let
530 + * http_pass / http_user / pubkey / securekey leak.
531 + *
532 + * @var string[]
533 + */
534 + protected static $sensitive_site_fields = array(
535 + 'privkey',
536 + 'pubkey',
537 + 'http_user',
538 + 'http_pass',
539 + 'adminname',
540 + 'securekey',
541 + // uniqueId is the per-site secure-mode signing identifier; the v2 schema
542 + // narrows it to edit context only (MWP-1541) and the single-site v1
543 + // callback uses get_website_by_id (SELECT wp.*) which carries it
544 + // through. Strip it here too so v1 stays consistent.
545 + 'uniqueId',
546 + );
547 +
548 + /**
549 + * Strip credential-shaped fields from a site object or array of sites
550 + * before returning it from a v1 REST callback. Operates in place on
551 + * each item; safe to call on a single site object, a list of objects,
552 + * or a list of associative arrays.
553 + *
554 + * @param mixed $data Either a site stdClass / array, or a list of either.
555 + * @return mixed The same $data with sensitive fields removed.
556 + */
557 + public static function strip_sensitive_site_fields( $data ) {
558 + if ( empty( $data ) ) {
559 + return $data;
560 + }
561 +
562 + // Detect single-item vs list. A list either has integer 0 as its
563 + // first key (sequential array) or the first element is itself an
564 + // array/object representing a site.
565 + $is_list = false;
566 + if ( is_array( $data ) ) {
567 + $first_key = array_key_first( $data );
568 + if ( null !== $first_key && is_int( $first_key ) ) {
569 + $is_list = true;
570 + }
571 + }
572 +
573 + if ( $is_list ) {
574 + foreach ( $data as $idx => $item ) {
575 + $data[ $idx ] = static::strip_sensitive_site_fields( $item );
576 + }
577 + return $data;
578 + }
579 +
580 + foreach ( static::$sensitive_site_fields as $field ) {
581 + if ( is_object( $data ) && property_exists( $data, $field ) ) {
582 + unset( $data->{$field} );
583 + } elseif ( is_array( $data ) && array_key_exists( $field, $data ) ) {
584 + unset( $data[ $field ] );
585 + }
586 + }
587 +
588 + return $data;
589 + }
590 +
591 + /**
522 592 * Method mainwp_rest_api_init()
523 593 *
524 594 * Makes sure the correct consumer key and secret are entered.
525 595 *
@@ -662,8 +732,9 @@
662 732
663 733 $method = $request->get_method();
664 734
665 735 if ( empty( $allow_methods ) || ! in_array( $method, $allow_methods ) ) {
736 + /* translators: %s: HTTP method name */
666 737 throw new MainWP_Exception( sprintf( esc_html__( 'Sorry, you are not allowed to do the %s method.', 'mainwp' ), ( isset( $methods_map[ $method ] ) ? $methods_map[ $method ] : '' ) ) ); //phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
667 738 }
668 739
669 740 return true;
@@ -792,8 +863,14 @@
792 863
793 864 // get data.
794 865 $data = MainWP_DB::instance()->get_websites_for_current_user( $params );
795 866
867 + // MWP-1542: strip credential fields before returning. The data
868 + // layer's full_data projection includes privkey / http_pass / etc.
869 + // for internal callers (cron, MainWP_Connect); REST consumers
870 + // must never see them.
871 + $data = static::strip_sensitive_site_fields( $data );
872 +
796 873 $result = $data;
797 874
798 875 if ( 'array' === $format && is_array( $data ) ) {
799 876 $result = array(
@@ -1187,11 +1264,12 @@
1187 1264 return $valid;
1188 1265 }
1189 1266
1190 1267 if ( true === $valid ) {
1191 - $data = array();
1192 - $params['extra_view'] = array( 'health_site_status' );
1193 - $websites = MainWP_DB::instance()->query( MainWP_DB::instance()->get_sql_websites_for_current_user() );
1268 + $data = array();
1269 + // Pass extra_view positionally (8th arg) so health_site_status is actually
1270 + // fetched; a $params['extra_view'] local never reaches this query.
1271 + $websites = MainWP_DB::instance()->query( MainWP_DB::instance()->get_sql_websites_for_current_user( false, null, 'wp.url', false, false, null, false, array( 'health_site_status' ) ) );
1194 1272 while ( $websites && ( $website = MainWP_DB::fetch_object( $websites ) ) ) {
1195 1273 $health_status = isset( $website->health_site_status ) ? json_decode( $website->health_site_status, true ) : array();
1196 1274 $hstatus = MainWP_Utility::get_site_health( $health_status );
1197 1275 $hval = $hstatus['val'];
@@ -1317,8 +1395,14 @@
1317 1395
1318 1396 // get data.
1319 1397 $data = MainWP_DB::instance()->get_websites_for_current_user( $params );
1320 1398
1399 + // MWP-1542: strip credential fields before returning. The data
1400 + // layer's full_data projection includes privkey / http_pass / etc.
1401 + // for internal callers (cron, MainWP_Connect); REST consumers
1402 + // must never see them.
1403 + $data = static::strip_sensitive_site_fields( $data );
1404 +
1321 1405 $result = $data;
1322 1406
1323 1407 if ( 'array' === $format && is_array( $data ) ) {
1324 1408 $result = array(
@@ -1374,8 +1458,14 @@
1374 1458
1375 1459 // get data.
1376 1460 $data = MainWP_DB::instance()->get_websites_for_current_user( $params );
1377 1461
1462 + // MWP-1542: strip credential fields before returning. The data
1463 + // layer's full_data projection includes privkey / http_pass / etc.
1464 + // for internal callers (cron, MainWP_Connect); REST consumers
1465 + // must never see them.
1466 + $data = static::strip_sensitive_site_fields( $data );
1467 +
1378 1468 $result = $data;
1379 1469
1380 1470 if ( 'array' === $format && is_array( $data ) ) {
1381 1471 $result = array(
@@ -1428,15 +1518,12 @@
1428 1518
1429 1519 // get data.
1430 1520 $data = MainWP_DB::instance()->get_website_by_id( $site_id, $selectGroups );
1431 1521
1432 - if ( ! empty( $data ) && property_exists( $data, 'privkey' ) ) {
1433 - unset( $data->privkey );
1434 - }
1435 -
1436 - if ( ! empty( $data ) && property_exists( $data, 'adminname' ) ) {
1437 - unset( $data->adminname );
1438 - }
1522 + // MWP-1542: strip every credential field, not just privkey/adminname.
1523 + // The pre-MWP-1542 partial strip let http_pass/http_user/pubkey/securekey
1524 + // pass through whenever the underlying row had them populated.
1525 + $data = static::strip_sensitive_site_fields( $data );
1439 1526
1440 1527 if ( $with_tags && ! empty( $data ) && property_exists( $data, 'wpgroups' ) && property_exists( $data, 'wpgroupids' ) ) {
1441 1528 $wpgroupids = explode( ',', $data->wpgroupids );
1442 1529 $wpgroups = explode( ',', $data->wpgroups );