Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
0.00% |
0 / 59 |
|
0.00% |
0 / 4 |
CRAP | |
0.00% |
0 / 1 |
| Atomic_Record_Jetpack_Token_Errors | |
0.00% |
0 / 58 |
|
0.00% |
0 / 4 |
506 | |
0.00% |
0 / 1 |
| signature_error_header | |
0.00% |
0 / 27 |
|
0.00% |
0 / 1 |
42 | |||
| is_jetpack_request | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
42 | |||
| check_ip | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
20 | |||
| check_ipv4 | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
42 | |||
| 1 | <?php |
| 2 | /** |
| 3 | * Atomic_Record_Jetpack_Token_Errors file. |
| 4 | * |
| 5 | * @package wpcomsh |
| 6 | */ |
| 7 | |
| 8 | /** |
| 9 | * Logs Jetpack token errors as response headers. |
| 10 | */ |
| 11 | class Atomic_Record_Jetpack_Token_Errors { |
| 12 | /** |
| 13 | * $error is a WP_Error (always) and contains a "signature_details" data property with this structure: |
| 14 | * The error_code has one of the following values: |
| 15 | * - malformed_token |
| 16 | * - malformed_user_id |
| 17 | * - unknown_token |
| 18 | * - could_not_sign |
| 19 | * - invalid_nonce |
| 20 | * - signature_mismatch |
| 21 | * |
| 22 | * @param WP_Error $error WP_Error instance. |
| 23 | */ |
| 24 | public static function signature_error_header( $error ) { |
| 25 | if ( headers_sent() ) { |
| 26 | return; |
| 27 | } |
| 28 | |
| 29 | if ( ! isset( $_SERVER['ATOMIC_SITE_ID'] ) && ! defined( 'ATOMIC_SITE_ID' ) ) { |
| 30 | return; |
| 31 | } |
| 32 | |
| 33 | if ( ! self::is_jetpack_request() ) { |
| 34 | return; |
| 35 | } |
| 36 | |
| 37 | $error_data = $error->get_error_data(); |
| 38 | if ( ! isset( $error_data['signature_details'] ) ) { |
| 39 | return; |
| 40 | } |
| 41 | header( |
| 42 | sprintf( |
| 43 | 'X-Jetpack-Signature-Error: %s', |
| 44 | $error->get_error_code() |
| 45 | ) |
| 46 | ); |
| 47 | header( |
| 48 | sprintf( |
| 49 | 'X-Jetpack-Signature-Error-Message: %s', |
| 50 | $error->get_error_message() |
| 51 | ) |
| 52 | ); |
| 53 | header( |
| 54 | sprintf( |
| 55 | 'X-Jetpack-Signature-Error-Details: %s', |
| 56 | base64_encode( wp_json_encode( $error_data['signature_details'], JSON_UNESCAPED_SLASHES ) ) // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode |
| 57 | ) |
| 58 | ); |
| 59 | } |
| 60 | |
| 61 | /** |
| 62 | * Checks the IP to see if it's a Jetpack request. |
| 63 | * |
| 64 | * Stolen from https://github.com/Automattic/vip-go-mu-plugins/pull/1301. |
| 65 | * |
| 66 | * @return bool |
| 67 | */ |
| 68 | public static function is_jetpack_request() { |
| 69 | // Filter by env. |
| 70 | if ( defined( 'WP_CLI' ) && WP_CLI ) { |
| 71 | return false; |
| 72 | } |
| 73 | |
| 74 | // Simple UA check to filter out most. |
| 75 | if ( false === stripos( $_SERVER['HTTP_USER_AGENT'], 'wpcomsh' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput |
| 76 | return false; |
| 77 | } |
| 78 | |
| 79 | // If it has a valid-looking UA, check the remote IP. |
| 80 | // From https://jetpack.com/support/hosting-faq/#jetpack-whitelist |
| 81 | $jetpack_ips = array( |
| 82 | '122.248.245.244', |
| 83 | '54.217.201.243', |
| 84 | '54.232.116.4', |
| 85 | '192.0.80.0/20', |
| 86 | '192.0.96.0/20', |
| 87 | '192.0.112.0/20', |
| 88 | '195.234.108.0/22', |
| 89 | ); |
| 90 | |
| 91 | // phpcs:ignore WordPress.Security.ValidatedSanitizedInput |
| 92 | return self::check_ip( $_SERVER['REMOTE_ADDR'], $jetpack_ips ) || ( isset( $_SERVER['HTTP_X_FORWARDED_FOR'] ) && self::check_ip( $_SERVER['HTTP_X_FORWARDED_FOR'], $jetpack_ips ) ); |
| 93 | } |
| 94 | |
| 95 | /** |
| 96 | * Checks if an IPv4 or IPv6 address is contained in the list of given IPs or subnets. |
| 97 | * |
| 98 | * @param string $request_ip IP to check. |
| 99 | * @param string|array $ips List of IPs or subnets (can be a string if only a single one). |
| 100 | * |
| 101 | * @return bool Whether the IP is valid. |
| 102 | */ |
| 103 | public static function check_ip( $request_ip, $ips ) { |
| 104 | if ( ! is_array( $ips ) ) { |
| 105 | $ips = array( $ips ); |
| 106 | } |
| 107 | |
| 108 | foreach ( $ips as $ip ) { |
| 109 | if ( self::check_ipv4( $request_ip, $ip ) ) { |
| 110 | return true; |
| 111 | } |
| 112 | } |
| 113 | |
| 114 | return false; |
| 115 | } |
| 116 | |
| 117 | /** |
| 118 | * Compares two IPv4 addresses. |
| 119 | * In case a subnet is given, it checks if it contains the request IP. |
| 120 | * |
| 121 | * @param string $request_ip IPv4 address to check. |
| 122 | * @param string $ip IPv4 address or subnet in CIDR notation. |
| 123 | * |
| 124 | * @return bool Whether the request IP matches the IP, or whether the request IP is within the CIDR subnet. |
| 125 | */ |
| 126 | public static function check_ipv4( $request_ip, $ip ) { |
| 127 | if ( ! filter_var( $request_ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) ) { |
| 128 | return false; |
| 129 | } |
| 130 | if ( false !== strpos( $ip, '/' ) ) { |
| 131 | list( $address, $netmask ) = explode( '/', $ip, 2 ); |
| 132 | if ( $netmask === '0' ) { |
| 133 | return filter_var( $address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ); |
| 134 | } |
| 135 | if ( $netmask < 0 || $netmask > 32 ) { |
| 136 | return false; |
| 137 | } |
| 138 | } else { |
| 139 | $address = $ip; |
| 140 | $netmask = 32; |
| 141 | } |
| 142 | |
| 143 | return 0 === substr_compare( sprintf( '%032b', ip2long( $request_ip ) ), sprintf( '%032b', ip2long( $address ) ), 0, $netmask ); |
| 144 | } |
| 145 | } |
| 146 | add_action( 'jetpack_verify_signature_error', array( 'Atomic_Record_Jetpack_Token_Errors', 'signature_error_header' ) ); |