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 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}
146add_action( 'jetpack_verify_signature_error', array( 'Atomic_Record_Jetpack_Token_Errors', 'signature_error_header' ) );