| @@ -427,12 +427,15 @@ | ||
| 427 | 427 | $http_user_plain = ! empty( $others['http_user_plain'] ) ? $others['http_user_plain'] : ''; |
| 428 | 428 | $http_pass_plain = ! empty( $others['http_pass_plain'] ) ? $others['http_pass_plain'] : ''; |
| 429 | 429 | |
| 430 | 430 | if ( $website && '' !== $what ) { |
| 431 | + // The nonce must never repeat. The Child stores every legacy signature it accepts, forever, | |
| 432 | + // and refuses a repeat as "This request has already been used"; signing is deterministic, | |
| 433 | + // so the nonce is the only thing that makes two requests for the same action differ. | |
| 431 | 434 | $data = array(); |
| 432 | 435 | $data['user'] = $website->adminname; |
| 433 | 436 | $data['function'] = $what; |
| 434 | - $data['nonce'] = wp_rand( 0, 9999 ); | |
| 437 | + $data['nonce'] = bin2hex( random_bytes( 16 ) ); | |
| 435 | 438 | $data['mainwpver'] = MainWP_System::$version; |
| 436 | 439 | |
| 437 | 440 | $params_filter = apply_filters( 'mainwp_pre_fetch_authed_data', false, $params, $what, $website, $verify_signature ); |
| 438 | 441 | if ( is_array( $params_filter ) && ! empty( $params_filter ) ) { |
| @@ -512,9 +515,9 @@ | ||
| 512 | 515 | 'base_function' => $what, |
| 513 | 516 | 'nonce' => $data['nonce'], |
| 514 | 517 | 'expires' => $ts + 60, |
| 515 | 518 | 'user' => $website->adminname, |
| 516 | - 'req_id' => wp_generate_uuid4(), | |
| 519 | + 'req_id' => wp_generate_uuid4(), | |
| 517 | 520 | ); |
| 518 | 521 | |
| 519 | 522 | if ( ! empty( $alt_user ) ) { |
| 520 | 523 | $data_sign_v2['alt_user'] = rawurlencode( $alt_user ); |
| @@ -615,9 +618,9 @@ | ||
| 615 | 618 | $compat_what = 'disconnect'; // to compatible, renew will call disconnect. |
| 616 | 619 | $data = array(); |
| 617 | 620 | $data['user'] = $website->adminname; |
| 618 | 621 | $data['function'] = $compat_what; |
| 619 | - $data['nonce'] = wp_rand( 0, 9999 ); | |
| 622 | + $data['nonce'] = bin2hex( random_bytes( 16 ) ); | |
| 620 | 623 | |
| 621 | 624 | $sign_value = $compat_what . $data['nonce']; // compatible format. |
| 622 | 625 | |
| 623 | 626 | $alg = false; |
| @@ -660,9 +663,9 @@ | ||
| 660 | 663 | 'base_function' => $compat_what, |
| 661 | 664 | 'nonce' => $data['nonce'], |
| 662 | 665 | 'expires' => $ts + 60, |
| 663 | 666 | 'user' => $website->adminname, |
| 664 | - 'req_id' => wp_generate_uuid4(), | |
| 667 | + 'req_id' => wp_generate_uuid4(), | |
| 665 | 668 | ); |
| 666 | 669 | $sign_value_v2 = wp_json_encode( $data_sign_v2 ); |
| 667 | 670 | |
| 668 | 671 | $sign_success_v2 = null; |
| @@ -730,9 +733,9 @@ | ||
| 730 | 733 | |
| 731 | 734 | $sign_success = null; |
| 732 | 735 | $alg = false; |
| 733 | 736 | $use_seclib = false; |
| 734 | - $nonce = wp_rand( 0, 9999 ); | |
| 737 | + $nonce = bin2hex( random_bytes( 16 ) ); | |
| 735 | 738 | |
| 736 | 739 | /** |
| 737 | 740 | * Current user global. |
| 738 | 741 | * |
| @@ -795,9 +798,9 @@ | ||
| 795 | 798 | 'where' => rawurlencode( $paramName ), |
| 796 | 799 | 'nonce' => $nonce, |
| 797 | 800 | 'expires' => $ts + 60, |
| 798 | 801 | 'user' => $website->adminname, |
| 799 | - 'req_id' => wp_generate_uuid4(), | |
| 802 | + 'req_id' => wp_generate_uuid4(), | |
| 800 | 803 | ); |
| 801 | 804 | |
| 802 | 805 | if ( ! empty( $alt_user ) ) { |
| 803 | 806 | $data_sign_v2['alt_user'] = rawurlencode( $alt_user ); |
| @@ -808,9 +811,9 @@ | ||
| 808 | 811 | $sign_value_v2 = wp_json_encode( $data_sign_v2 ); |
| 809 | 812 | |
| 810 | 813 | if ( MainWP_Connect_Lib::is_use_fallback_sec_lib( $website ) ) { |
| 811 | 814 | $sign_success_v2 = MainWP_Connect_Lib::connect_sign( $sign_value_v2, $signature_v2, base64_decode( $website->privkey ), $website->id ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible. |
| 812 | - $use_seclib = true; | |
| 815 | + $use_seclib = true; | |
| 813 | 816 | } elseif ( function_exists( 'openssl_verify' ) ) { |
| 814 | 817 | $alg = MainWP_System_Utility::get_connect_sign_algorithm( $website ); |
| 815 | 818 | $sign_success_v2 = static::connect_sign( $sign_value_v2, $signature_v2, base64_decode( $website->privkey ), $alg, $website->id ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode used for http encoding compatible. |
| 816 | 819 | if ( false !== $alg ) { |
| @@ -1602,11 +1605,18 @@ | ||
| 1602 | 1605 | * @since 4.5.1.1 |
| 1603 | 1606 | */ |
| 1604 | 1607 | do_action( 'mainwp_fetch_url_authed', $website, $information, $what, $params, $others ); |
| 1605 | 1608 | |
| 1606 | - if ( is_array( $information ) && isset( $information['sync'] ) && ! empty( $information['sync'] ) ) { | |
| 1607 | - MainWP_Sync::sync_information_array( $website, $information['sync'] ); | |
| 1608 | - unset( $information['sync'] ); | |
| 1609 | + if ( is_array( $information ) ) { | |
| 1610 | + if ( isset( $information['sync'] ) && ! empty( $information['sync'] ) ) { | |
| 1611 | + MainWP_Sync::sync_information_array( $website, $information['sync'] ); | |
| 1612 | + unset( $information['sync'] ); | |
| 1613 | + } | |
| 1614 | + | |
| 1615 | + if ( isset( $information['sync_partial'] ) && is_array( $information['sync_partial'] ) && ! empty( $information['sync_partial'] ) ) { | |
| 1616 | + MainWP_Sync::sync_partial_information( $website, $information['sync_partial'] ); | |
| 1617 | + unset( $information['sync_partial'] ); | |
| 1618 | + } | |
| 1609 | 1619 | } |
| 1610 | 1620 | |
| 1611 | 1621 | if ( $updating_website ) { |
| 1612 | 1622 | /** |
| @@ -1661,8 +1671,72 @@ | ||
| 1661 | 1671 | $sslVersion = 0, |
| 1662 | 1672 | $others = array(), |
| 1663 | 1673 | &$output = array() |
| 1664 | 1674 | ) { |
| 1675 | + | |
| 1676 | + /** | |
| 1677 | + * Filter to mock the fetch_url_not_authed response before any HTTP request occurs. | |
| 1678 | + * | |
| 1679 | + * This filter fires early, before the HTTP request is performed, allowing | |
| 1680 | + * tests to bypass child site communication entirely. | |
| 1681 | + * | |
| 1682 | + * SECURITY WARNING - TEST ONLY: | |
| 1683 | + * This filter ONLY fires when ALL of the following conditions are met: | |
| 1684 | + * 1. MAINWP_TESTING_MODE constant is defined and true. | |
| 1685 | + * 2. A PHPUnit test harness constant is present (WP_TESTS_DOMAIN, | |
| 1686 | + * PHPUNIT_COMPOSER_INSTALL, or WP_TESTS_DIR). | |
| 1687 | + * | |
| 1688 | + * These checks prevent malicious code from defining MAINWP_TESTING_MODE | |
| 1689 | + * in production to spoof child site responses. | |
| 1690 | + * | |
| 1691 | + * IMPORTANT: MAINWP_TESTING_MODE must ONLY be defined in the PHPUnit bootstrap | |
| 1692 | + * file (tests/bootstrap.php). Defining it in production code, wp-config.php, | |
| 1693 | + * or plugin files would create a security vulnerability allowing response | |
| 1694 | + * spoofing. | |
| 1695 | + * | |
| 1696 | + * @since 6.2.1 | |
| 1697 | + * | |
| 1698 | + * @param mixed $pre_result Return non-false to short-circuit the request and return this value. | |
| 1699 | + * @param string $url URL to fetch. | |
| 1700 | + * @param string $admin Admin username or identifier. | |
| 1701 | + * @param string $what Action being performed. | |
| 1702 | + * @param array|null $params Request parameters. | |
| 1703 | + * @param bool $pForceFetch Whether to force the fetch. | |
| 1704 | + * @param bool|null $verifyCertificate Whether to verify the SSL certificate. | |
| 1705 | + * @param string|null $http_user HTTP authentication username. | |
| 1706 | + * @param string|null $http_pass HTTP authentication password. | |
| 1707 | + * @param int $sslVersion SSL version. | |
| 1708 | + * @param array $others Additional request options. | |
| 1709 | + * @param array $output Output data passed by reference. | |
| 1710 | + * | |
| 1711 | + * @return mixed Array or other value to return early, false to proceed normally. | |
| 1712 | + */ | |
| 1713 | + $is_phpunit_env = defined( 'WP_TESTS_DOMAIN' ) || defined( 'PHPUNIT_COMPOSER_INSTALL' ) || ( defined( 'WP_TESTS_DIR' ) && WP_TESTS_DIR ); | |
| 1714 | + | |
| 1715 | + if ( defined( 'MAINWP_TESTING_MODE' ) && MAINWP_TESTING_MODE && $is_phpunit_env ) { | |
| 1716 | + $pre_result = apply_filters_ref_array( | |
| 1717 | + 'mainwp_fetch_url_not_authed_pre', | |
| 1718 | + array( | |
| 1719 | + false, | |
| 1720 | + $url, | |
| 1721 | + $admin, | |
| 1722 | + $what, | |
| 1723 | + $params, | |
| 1724 | + $pForceFetch, | |
| 1725 | + $verifyCertificate, | |
| 1726 | + $http_user, | |
| 1727 | + $http_pass, | |
| 1728 | + $sslVersion, | |
| 1729 | + $others, | |
| 1730 | + &$output, | |
| 1731 | + ) | |
| 1732 | + ); | |
| 1733 | + | |
| 1734 | + if ( false !== $pre_result ) { | |
| 1735 | + return $pre_result; | |
| 1736 | + } | |
| 1737 | + } | |
| 1738 | + | |
| 1665 | 1739 | unset( $pForceFetch ); |
| 1666 | 1740 | |
| 1667 | 1741 | if ( empty( $params ) ) { |
| 1668 | 1742 | $params = array(); |
| @@ -2058,9 +2132,9 @@ | ||
| 2058 | 2132 | MainWP_Logger::instance()->log_execution_time( 'fetch_url_site :: [url=' . $url . ']' ); |
| 2059 | 2133 | |
| 2060 | 2134 | $thr_error = null; |
| 2061 | 2135 | |
| 2062 | - if ( in_array( $what, array( 'installplugintheme', 'upgradeplugintheme', 'upgradetranslation', 'upgrade', 'stats', 'renew', 'reconnect' ), true ) ) { | |
| 2136 | + if ( in_array( $what, array( 'installplugintheme', 'upgradeplugintheme', 'upgradetranslation', 'process_premium_updates', 'upgrade', 'stats', 'renew', 'reconnect' ), true ) ) { | |
| 2063 | 2137 | MainWP_Cache_Helper::invalidate_cache_group( MainWP_Cache_Helper::CGR_UPDATES ); |
| 2064 | 2138 | MainWP_Cache_Warm_Helper::invalidate_pages_by_site_actions( $what ); |
| 2065 | 2139 | } |
| 2066 | 2140 | |