Completed
Push — update/videopress-transcode-al... ( ffe79d...2d3973 )
by Kirk
13:06 queued 05:54
created

connection/legacy/class.jetpack-signature.php (1 issue)

Labels
Severity

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 * The Jetpack Connection signature class file.
4
 *
5
 * @package automattic/jetpack-connection
6
 */
7
8
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
9
10
/**
11
 * The Jetpack Connection signature class that is used to sign requests.
12
 */
13
class Jetpack_Signature {
14
	/**
15
	 * Token part of the access token.
16
	 *
17
	 * @access public
18
	 * @var string
19
	 */
20
	public $token;
21
22
	/**
23
	 * Access token secret.
24
	 *
25
	 * @access public
26
	 * @var string
27
	 */
28
	public $secret;
29
30
	/**
31
	 * The current request URL.
32
	 *
33
	 * @access public
34
	 * @var string
35
	 */
36
	public $current_request_url;
37
38
	/**
39
	 * Constructor.
40
	 *
41
	 * @param array $access_token Access token.
42
	 * @param int   $time_diff    Timezone difference (in seconds).
43
	 */
44
	public function __construct( $access_token, $time_diff = 0 ) {
45
		$secret = explode( '.', $access_token );
46
		if ( 2 !== count( $secret ) ) {
47
			return;
48
		}
49
50
		$this->token     = $secret[0];
51
		$this->secret    = $secret[1];
52
		$this->time_diff = $time_diff;
0 ignored issues
show
The property time_diff does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
53
	}
54
55
	/**
56
	 * Sign the current request.
57
	 *
58
	 * @todo Implement a proper nonce verification.
59
	 *
60
	 * @param array $override Optional arguments to override the ones from the current request.
61
	 * @return string|WP_Error Request signature, or a WP_Error on failure.
62
	 */
63
	public function sign_current_request( $override = array() ) {
64
		if ( isset( $override['scheme'] ) ) {
65
			$scheme = $override['scheme'];
66
			if ( ! in_array( $scheme, array( 'http', 'https' ), true ) ) {
67
				return new WP_Error( 'invalid_scheme', 'Invalid URL scheme' );
68
			}
69
		} else {
70
			if ( is_ssl() ) {
71
				$scheme = 'https';
72
			} else {
73
				$scheme = 'http';
74
			}
75
		}
76
77
		$host_port = isset( $_SERVER['HTTP_X_FORWARDED_PORT'] ) ? $_SERVER['HTTP_X_FORWARDED_PORT'] : $_SERVER['SERVER_PORT'];
78
		$host_port = intval( $host_port );
79
80
		/**
81
		 * Note: This port logic is tested in the Jetpack_Cxn_Tests->test__server_port_value() test.
82
		 * Please update the test if any changes are made in this logic.
83
		 */
84
		if ( is_ssl() ) {
85
			// 443: Standard Port
86
			// 80: Assume we're behind a proxy without X-Forwarded-Port. Hardcoding "80" here means most sites
87
			// with SSL termination proxies (self-served, Cloudflare, etc.) don't need to fiddle with
88
			// the JETPACK_SIGNATURE__HTTPS_PORT constant. The code also implies we can't talk to a
89
			// site at https://example.com:80/ (which would be a strange configuration).
90
			// JETPACK_SIGNATURE__HTTPS_PORT: Set this constant in wp-config.php to the back end webserver's port
91
			// if the site is behind a proxy running on port 443 without
92
			// X-Forwarded-Port and the back end's port is *not* 80. It's better,
93
			// though, to configure the proxy to send X-Forwarded-Port.
94
			$https_port = defined( 'JETPACK_SIGNATURE__HTTPS_PORT' ) ? JETPACK_SIGNATURE__HTTPS_PORT : 443;
95
			$port       = in_array( $host_port, array( 443, 80, $https_port ), true ) ? '' : $host_port;
96
		} else {
97
			// 80: Standard Port
98
			// JETPACK_SIGNATURE__HTTPS_PORT: Set this constant in wp-config.php to the back end webserver's port
99
			// if the site is behind a proxy running on port 80 without
100
			// X-Forwarded-Port. It's better, though, to configure the proxy to
101
			// send X-Forwarded-Port.
102
			$http_port = defined( 'JETPACK_SIGNATURE__HTTP_PORT' ) ? JETPACK_SIGNATURE__HTTP_PORT : 80;
103
			$port      = in_array( $host_port, array( 80, $http_port ), true ) ? '' : $host_port;
104
		}
105
106
		$this->current_request_url = "{$scheme}://{$_SERVER['HTTP_HOST']}:{$port}" . stripslashes( $_SERVER['REQUEST_URI'] );
107
108
		if ( array_key_exists( 'body', $override ) && ! empty( $override['body'] ) ) {
109
			$body = $override['body'];
110
		} elseif ( 'POST' === strtoupper( $_SERVER['REQUEST_METHOD'] ) ) {
111
			$body = isset( $GLOBALS['HTTP_RAW_POST_DATA'] ) ? $GLOBALS['HTTP_RAW_POST_DATA'] : null;
112
113
			// Convert the $_POST to the body, if the body was empty. This is how arrays are hashed
114
			// and encoded on the Jetpack side.
115
			if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
116
				// phpcs:ignore WordPress.Security.NonceVerification.Missing
117
				if ( empty( $body ) && is_array( $_POST ) && count( $_POST ) > 0 ) {
118
					$body = $_POST; // phpcs:ignore WordPress.Security.NonceVerification.Missing
119
				}
120
			}
121
		} elseif ( 'PUT' === strtoupper( $_SERVER['REQUEST_METHOD'] ) ) {
122
			// This is a little strange-looking, but there doesn't seem to be another way to get the PUT body.
123
			$raw_put_data = file_get_contents( 'php://input' );
124
			parse_str( $raw_put_data, $body );
125
126
			if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
127
				$put_data = json_decode( $raw_put_data, true );
128
				if ( is_array( $put_data ) && count( $put_data ) > 0 ) {
129
					$body = $put_data;
130
				}
131
			}
132
		} else {
133
			$body = null;
134
		}
135
136
		if ( empty( $body ) ) {
137
			$body = null;
138
		}
139
140
		$a = array();
141
		foreach ( array( 'token', 'timestamp', 'nonce', 'body-hash' ) as $parameter ) {
142
			if ( isset( $override[ $parameter ] ) ) {
143
				$a[ $parameter ] = $override[ $parameter ];
144
			} else {
145
				// phpcs:ignore WordPress.Security.NonceVerification.Recommended
146
				$a[ $parameter ] = isset( $_GET[ $parameter ] ) ? stripslashes( $_GET[ $parameter ] ) : '';
147
			}
148
		}
149
150
		$method = isset( $override['method'] ) ? $override['method'] : $_SERVER['REQUEST_METHOD'];
151
		return $this->sign_request( $a['token'], $a['timestamp'], $a['nonce'], $a['body-hash'], $method, $this->current_request_url, $body, true );
152
	}
153
154
	/**
155
	 * Sign a specified request.
156
	 *
157
	 * @todo Having body_hash v. body-hash is annoying. Refactor to accept an array?
158
	 * @todo Use wp_json_encode() instead of json_encode()?
159
	 * @todo Use wp_parse_url() instead of parse_url()?
160
	 *
161
	 * @param string $token            Request token.
162
	 * @param int    $timestamp        Timestamp of the request.
163
	 * @param string $nonce            Request nonce.
164
	 * @param string $body_hash        Request body hash.
165
	 * @param string $method           Request method.
166
	 * @param string $url              Request URL.
167
	 * @param mixed  $body             Request body.
168
	 * @param bool   $verify_body_hash Whether to verify the body hash against the body.
169
	 * @return string|WP_Error Request signature, or a WP_Error on failure.
170
	 */
171
	public function sign_request( $token = '', $timestamp = 0, $nonce = '', $body_hash = '', $method = '', $url = '', $body = null, $verify_body_hash = true ) {
172
		if ( ! $this->secret ) {
173
			return new WP_Error( 'invalid_secret', 'Invalid secret' );
174
		}
175
176
		if ( ! $this->token ) {
177
			return new WP_Error( 'invalid_token', 'Invalid token' );
178
		}
179
180
		list( $token ) = explode( '.', $token );
181
182
		$signature_details = compact( 'token', 'timestamp', 'nonce', 'body_hash', 'method', 'url' );
183
184
		if ( 0 !== strpos( $token, "$this->token:" ) ) {
185
			return new WP_Error( 'token_mismatch', 'Incorrect token', compact( 'signature_details' ) );
186
		}
187
188
		// If we got an array at this point, let's encode it, so we can see what it looks like as a string.
189
		if ( is_array( $body ) ) {
190
			if ( count( $body ) > 0 ) {
191
				// phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode
192
				$body = json_encode( $body );
193
194
			} else {
195
				$body = '';
196
			}
197
		}
198
199
		$required_parameters = array( 'token', 'timestamp', 'nonce', 'method', 'url' );
200
		if ( ! is_null( $body ) ) {
201
			$required_parameters[] = 'body_hash';
202
			if ( ! is_string( $body ) ) {
203
				return new WP_Error( 'invalid_body', 'Body is malformed.', compact( 'signature_details' ) );
204
			}
205
		}
206
207
		foreach ( $required_parameters as $required ) {
208
			if ( ! is_scalar( $$required ) ) {
209
				return new WP_Error( 'invalid_signature', sprintf( 'The required "%s" parameter is malformed.', str_replace( '_', '-', $required ) ), compact( 'signature_details' ) );
210
			}
211
212 View Code Duplication
			if ( ! strlen( $$required ) ) {
213
				return new WP_Error( 'invalid_signature', sprintf( 'The required "%s" parameter is missing.', str_replace( '_', '-', $required ) ), compact( 'signature_details' ) );
214
			}
215
		}
216
217
		if ( empty( $body ) ) {
218
			if ( $body_hash ) {
219
				return new WP_Error( 'invalid_body_hash', 'Invalid body hash for empty body.', compact( 'signature_details' ) );
220
			}
221
		} else {
222
			$connection = new Connection_Manager();
223
			if ( $verify_body_hash && $connection->sha1_base64( $body ) !== $body_hash ) {
224
				return new WP_Error( 'invalid_body_hash', 'The body hash does not match.', compact( 'signature_details' ) );
225
			}
226
		}
227
228
		// phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url
229
		$parsed = parse_url( $url );
230
		if ( ! isset( $parsed['host'] ) ) {
231
			return new WP_Error( 'invalid_signature', sprintf( 'The required "%s" parameter is malformed.', 'url' ), compact( 'signature_details' ) );
232
		}
233
234
		if ( ! empty( $parsed['port'] ) ) {
235
			$port = $parsed['port'];
236
		} else {
237
			if ( 'http' === $parsed['scheme'] ) {
238
				$port = 80;
239
			} elseif ( 'https' === $parsed['scheme'] ) {
240
				$port = 443;
241
			} else {
242
				return new WP_Error( 'unknown_scheme_port', "The scheme's port is unknown", compact( 'signature_details' ) );
243
			}
244
		}
245
246 View Code Duplication
		if ( ! ctype_digit( "$timestamp" ) || 10 < strlen( $timestamp ) ) { // If Jetpack is around in 275 years, you can blame mdawaffe for the bug.
247
			return new WP_Error( 'invalid_signature', sprintf( 'The required "%s" parameter is malformed.', 'timestamp' ), compact( 'signature_details' ) );
248
		}
249
250
		$local_time = $timestamp - $this->time_diff;
251 View Code Duplication
		if ( $local_time < time() - 600 || $local_time > time() + 300 ) {
252
			return new WP_Error( 'invalid_signature', 'The timestamp is too old.', compact( 'signature_details' ) );
253
		}
254
255 View Code Duplication
		if ( 12 < strlen( $nonce ) || preg_match( '/[^a-zA-Z0-9]/', $nonce ) ) {
256
			return new WP_Error( 'invalid_signature', sprintf( 'The required "%s" parameter is malformed.', 'nonce' ), compact( 'signature_details' ) );
257
		}
258
259
		$normalized_request_pieces = array(
260
			$token,
261
			$timestamp,
262
			$nonce,
263
			$body_hash,
264
			strtoupper( $method ),
265
			strtolower( $parsed['host'] ),
266
			$port,
267
			$parsed['path'],
268
			// Normalized Query String.
269
		);
270
271
		$normalized_request_pieces      = array_merge( $normalized_request_pieces, $this->normalized_query_parameters( isset( $parsed['query'] ) ? $parsed['query'] : '' ) );
272
		$flat_normalized_request_pieces = array();
273
		foreach ( $normalized_request_pieces as $piece ) {
274
			if ( is_array( $piece ) ) {
275
				foreach ( $piece as $subpiece ) {
276
					$flat_normalized_request_pieces[] = $subpiece;
277
				}
278
			} else {
279
				$flat_normalized_request_pieces[] = $piece;
280
			}
281
		}
282
		$normalized_request_pieces = $flat_normalized_request_pieces;
283
284
		$normalized_request_string = join( "\n", $normalized_request_pieces ) . "\n";
285
286
		// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
287
		return base64_encode( hash_hmac( 'sha1', $normalized_request_string, $this->secret, true ) );
288
	}
289
290
	/**
291
	 * Retrieve and normalize the parameters from a query string.
292
	 *
293
	 * @param string $query_string Query string.
294
	 * @return array Normalized query string parameters.
295
	 */
296
	public function normalized_query_parameters( $query_string ) {
297
		parse_str( $query_string, $array );
298
		if ( get_magic_quotes_gpc() ) {
299
			$array = stripslashes_deep( $array );
300
		}
301
302
		unset( $array['signature'] );
303
304
		$names  = array_keys( $array );
305
		$values = array_values( $array );
306
307
		$names  = array_map( array( $this, 'encode_3986' ), $names );
308
		$values = array_map( array( $this, 'encode_3986' ), $values );
309
310
		$pairs = array_map( array( $this, 'join_with_equal_sign' ), $names, $values );
311
312
		sort( $pairs );
313
314
		return $pairs;
315
	}
316
317
	/**
318
	 * Encodes a string or array of strings according to RFC 3986.
319
	 *
320
	 * @param string|array $string_or_array String or array to encode.
321
	 * @return string|array URL-encoded string or array.
322
	 */
323
	public function encode_3986( $string_or_array ) {
324
		if ( is_array( $string_or_array ) ) {
325
			return array_map( array( $this, 'encode_3986' ), $string_or_array );
326
		}
327
328
		return rawurlencode( $string_or_array );
329
	}
330
331
	/**
332
	 * Concatenates a parameter name and a parameter value with an equals sign between them.
333
	 * Supports one-dimensional arrays as `$value`.
334
	 *
335
	 * @param string $name  Parameter name.
336
	 * @param mixed  $value Parameter value.
337
	 * @return string A pair with parameter name and value (e.g. `name=value`).
338
	 */
339
	public function join_with_equal_sign( $name, $value ) {
340
		if ( is_array( $value ) ) {
341
			$result = array();
342
			foreach ( $value as $array_key => $array_value ) {
343
				$result[] = $name . '[' . $array_key . ']=' . $array_value;
344
			}
345
			return $result;
346
		}
347
		return "{$name}={$value}";
348
	}
349
}
350