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