PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3
Jetpack – WP Security, Backup, Speed, & Growth v16.3
16.3 16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 All 508 releases
← All changes | jetpack_vendor/automattic/jetpack-waf/src/class-waf-request.php +407 -0 16.2-beta → 16.3 View file →
@@ -1,0 +1,407 @@
1 +<?php
2 +/**
3 + * HTTP request representation specific for the WAF.
4 + *
5 + * @package automattic/jetpack-waf
6 + */
7 +
8 +namespace Automattic\Jetpack\Waf;
9 +
10 +require_once __DIR__ . '/functions.php';
11 +
12 +<<<'PHAN'
13 +@phan-type RequestFile = array{ name: string, filename: string }
14 +PHAN;
15 +
16 +/**
17 + * Request representation.
18 + */
19 +class Waf_Request {
20 + /**
21 + * The request URL, broken into three pieces: the host, the filename, and the query string
22 + *
23 + * @example for `https://wordpress.com/index.php?myvar=red`
24 + * $this->url = [ 'https://wordpress.com', '/index.php', '?myvar=red' ]
25 + * @var array{0: string, 1: string, 2: string}|null
26 + */
27 + protected $url = null;
28 +
29 + /**
30 + * Trusted proxies.
31 + *
32 + * @var array List of trusted proxy IP addresses.
33 + */
34 + private $trusted_proxies = array();
35 +
36 + /**
37 + * Trusted headers.
38 + *
39 + * @var array List of headers to trust from the trusted proxies.
40 + */
41 + private $trusted_headers = array();
42 +
43 + /**
44 + * Sets the list of IP addresses for the proxies to trust. Trusted headers will only be accepted as the
45 + * user IP address from these IP adresses.
46 + *
47 + * Popular choices include:
48 + * - 192.168.0.1
49 + * - 10.0.0.1
50 + *
51 + * @param array $proxies List of proxy IP addresses.
52 + * @return void
53 + */
54 + public function set_trusted_proxies( $proxies ) {
55 + $this->trusted_proxies = (array) $proxies;
56 + }
57 +
58 + /**
59 + * Sets the list of headers to be trusted from the proxies. These headers will only be taken into account
60 + * if the request comes from a trusted proxy as configured with set_trusted_proxies().
61 + *
62 + * Popular choices include:
63 + * - HTTP_CLIENT_IP
64 + * - HTTP_X_FORWARDED_FOR
65 + * - HTTP_X_FORWARDED
66 + * - HTTP_X_CLUSTER_CLIENT_IP
67 + * - HTTP_FORWARDED_FOR
68 + * - HTTP_FORWARDED
69 + *
70 + * @param array $headers List of HTTP header strings.
71 + * @return void
72 + */
73 + public function set_trusted_headers( $headers ) {
74 + $this->trusted_headers = (array) $headers;
75 + }
76 +
77 + /**
78 + * Determines the users real IP address based on the settings passed to set_trusted_proxies() and
79 + * set_trusted_headers() before. On CLI, this will be null.
80 + *
81 + * @return string|null
82 + */
83 + public function get_real_user_ip_address() {
84 + $remote_addr = ! empty( $_SERVER['REMOTE_ADDR'] ) ? wp_unslash( $_SERVER['REMOTE_ADDR'] ) : null; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
85 +
86 + if ( in_array( $remote_addr, $this->trusted_proxies, true ) ) {
87 + $ip_by_header = $this->get_ip_by_header( array_merge( $this->trusted_headers, array( 'REMOTE_ADDR' ) ) );
88 + if ( ! empty( $ip_by_header ) ) {
89 + return $ip_by_header;
90 + }
91 + }
92 +
93 + return $remote_addr;
94 + }
95 +
96 + /**
97 + * Iterates through a given list of HTTP headers and attempts to get the IP address from the header that
98 + * a proxy sends along. Make sure you trust the IP address before calling this method.
99 + *
100 + * @param array $headers The list of headers to check.
101 + * @return string|null
102 + */
103 + private function get_ip_by_header( $headers ) {
104 + foreach ( $headers as $key ) {
105 + if ( isset( $_SERVER[ $key ] ) ) {
106 + foreach ( explode( ',', wp_unslash( $_SERVER[ $key ] ) ) as $ip ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- filter_var is applied below.
107 + $ip = trim( $ip );
108 +
109 + if ( filter_var( $ip, FILTER_VALIDATE_IP ) !== false ) {
110 + return $ip;
111 + }
112 + }
113 + }
114 + }
115 +
116 + return null;
117 + }
118 +
119 + /**
120 + * Returns the headers that were sent with this request
121 + *
122 + * @return array{0: string, 1: scalar}[]
123 + */
124 + public function get_headers() {
125 + $value = array();
126 + $has_content_length = false;
127 + foreach ( $_SERVER as $k => $v ) {
128 + $k = strtolower( $k );
129 + if ( 'http_' === substr( $k, 0, 5 ) ) {
130 + $value[] = array( $this->normalize_header_name( substr( $k, 5 ) ), $v );
131 + } elseif ( 'content_type' === $k && '' !== $v ) {
132 + $value[] = array( 'content-type', $v );
133 + } elseif ( 'content_length' === $k && '' !== $v ) {
134 + $has_content_length = true;
135 + $value[] = array( 'content-length', $v );
136 + }
137 + }
138 + if ( ! $has_content_length ) {
139 + $value[] = array( 'content-length', '0' );
140 + }
141 +
142 + return $value;
143 + }
144 +
145 + /**
146 + * Returns the value of a specific header that was sent with this request
147 + *
148 + * @param string $name The name of the header to retrieve.
149 + * @return string
150 + */
151 + public function get_header( $name ) {
152 + $name = $this->normalize_header_name( $name );
153 + foreach ( $this->get_headers() as list( $header_name, $header_value ) ) {
154 + if ( $header_name === $name ) {
155 + return $header_value;
156 + }
157 + }
158 + return '';
159 + }
160 +
161 + /**
162 + * Change a header name to all-lowercase and replace spaces and underscores with dashes.
163 + *
164 + * @param string $name The header name to normalize.
165 + * @return string
166 + */
167 + public function normalize_header_name( $name ) {
168 + return str_replace( array( ' ', '_' ), '-', strtolower( $name ) );
169 + }
170 +
171 + /**
172 + * Get the method for this request (GET, POST, etc).
173 + *
174 + * @return string
175 + */
176 + public function get_method() {
177 + return isset( $_SERVER['REQUEST_METHOD'] )
178 + ? filter_var( wp_unslash( $_SERVER['REQUEST_METHOD'] ), FILTER_DEFAULT )
179 + : '';
180 + }
181 +
182 + /**
183 + * Get the protocol for this request (HTTP, HTTPS, etc)
184 + *
185 + * @return string
186 + */
187 + public function get_protocol() {
188 + return isset( $_SERVER['SERVER_PROTOCOL'] )
189 + ? filter_var( wp_unslash( $_SERVER['SERVER_PROTOCOL'] ), FILTER_DEFAULT )
190 + : '';
191 + }
192 +
193 + /**
194 + * Returns the URL parts for this request.
195 + *
196 + * @see $this->url
197 + * @return array{0: string, 1: string, 2: string}
198 + */
199 + protected function get_url() {
200 + if ( null !== $this->url ) {
201 + return $this->url;
202 + }
203 +
204 + $uri = isset( $_SERVER['REQUEST_URI'] ) ? filter_var( wp_unslash( $_SERVER['REQUEST_URI'] ), FILTER_DEFAULT ) : '/';
205 + if ( false !== strpos( $uri, '?' ) ) {
206 + // remove the query string (we'll pull it from elsewhere later)
207 + $uri = urldecode( substr( $uri, 0, strpos( $uri, '?' ) ) );
208 + } else {
209 + $uri = urldecode( $uri );
210 + }
211 + $query_string = isset( $_SERVER['QUERY_STRING'] ) ? '?' . filter_var( wp_unslash( $_SERVER['QUERY_STRING'] ), FILTER_DEFAULT ) : '';
212 + if ( 1 === preg_match( '/^https?:\/\//', $uri ) ) {
213 + // sometimes $_SERVER[REQUEST_URI] already includes the full domain name
214 + $uri_host = substr( $uri, 0, strpos( $uri, '/', 8 ) );
215 + $uri_path = substr( $uri, strlen( $uri_host ) );
216 + $this->url = array( $uri_host, $uri_path, $query_string );
217 + } else {
218 + // otherwise build the URI manually
219 + $uri_scheme = ( ! empty( $_SERVER['HTTPS'] ) && 'off' !== $_SERVER['HTTPS'] )
220 + ? 'https'
221 + : 'http';
222 + $uri_host = isset( $_SERVER['HTTP_HOST'] )
223 + ? filter_var( wp_unslash( $_SERVER['HTTP_HOST'] ), FILTER_DEFAULT )
224 + : (
225 + isset( $_SERVER['SERVER_NAME'] )
226 + ? filter_var( wp_unslash( $_SERVER['SERVER_NAME'] ), FILTER_DEFAULT )
227 + : ''
228 + );
229 + $uri_port = isset( $_SERVER['SERVER_PORT'] )
230 + ? filter_var( wp_unslash( $_SERVER['SERVER_PORT'] ), FILTER_SANITIZE_NUMBER_INT )
231 + : '';
232 + // we only need to include the port if it's non-standard
233 + if ( $uri_port && ( 'http' === $uri_scheme && '80' !== $uri_port || 'https' === $uri_scheme && '443' !== $uri_port ) ) {
234 + $uri_port = ':' . $uri_port;
235 + } else {
236 + $uri_port = '';
237 + }
238 + $this->url = array(
239 + $uri_scheme . '://' . $uri_host . $uri_port,
240 + $uri,
241 + $query_string,
242 + );
243 + }
244 + return $this->url;
245 + }
246 +
247 + /**
248 + * Get the requested URI
249 + *
250 + * @param boolean $include_host If true, the scheme and domain will be included in the returned string (i.e. 'https://wordpress.com/index.php).
251 + * If false, only the requested URI path will be returned (i.e. '/index.php').
252 + * @return string
253 + */
254 + public function get_uri( $include_host = false ) {
255 + list( $host, $file, $query ) = $this->get_url();
256 +
257 + return ( $include_host ? $host : '' ) . $file . $query;
258 + }
259 +
260 + /**
261 + * Return the filename part of the request
262 + *
263 + * @example for 'https://wordpress.com/some/page?id=5', return '/some/page'
264 + * @return string
265 + */
266 + public function get_filename() {
267 + return $this->get_url()[1];
268 + }
269 +
270 + /**
271 + * Return the basename part of the request
272 + *
273 + * @example for 'https://wordpress.com/some/page.php?id=5', return 'page.php'
274 + * @return string
275 + */
276 + public function get_basename() {
277 + // Get the filename part of the request
278 + $filename = $this->get_filename();
279 + // Normalize slashes
280 + $filename = str_replace( '\\', '/', $filename );
281 + // Remove trailing slashes
282 + $filename = rtrim( $filename, '/' );
283 + // Return the basename
284 + $offset = strrpos( $filename, '/' );
285 + return $offset !== false ? substr( $filename, $offset + 1 ) : $filename;
286 + }
287 +
288 + /**
289 + * Return the query string. If present, it will be prefixed with '?'. Otherwise, it will be an empty string.
290 + *
291 + * @return string
292 + */
293 + public function get_query_string() {
294 + return $this->get_url()[2];
295 + }
296 +
297 + /**
298 + * Returns the request body.
299 + *
300 + * @return string
301 + */
302 + public function get_body() {
303 + $body = file_get_contents( 'php://input' );
304 + return false === $body ? '' : $body;
305 + }
306 +
307 + /**
308 + * Returns the cookies
309 + *
310 + * @return array{string, scalar}[]
311 + */
312 + public function get_cookies() {
313 + return flatten_array( $_COOKIE );
314 + }
315 +
316 + /**
317 + * Returns the GET variables
318 + *
319 + * @return array{string, scalar}[]
320 + */
321 + public function get_get_vars() {
322 + return flatten_array( $_GET );
323 + }
324 +
325 + /**
326 + * Returns the POST variables from a JSON body
327 + *
328 + * @return array{string, scalar}[]
329 + */
330 + private function get_json_post_vars() {
331 + $decoded_json = json_decode( $this->get_body(), true ) ?? array();
332 + return flatten_array( $decoded_json, 'json', true );
333 + }
334 +
335 + /**
336 + * Returns the POST variables from a urlencoded body
337 + *
338 + * @return array{string, scalar}[]
339 + */
340 + private function get_urlencoded_post_vars() {
341 + parse_str( $this->get_body(), $params );
342 + return flatten_array( $params );
343 + }
344 +
345 + /**
346 + * Returns the POST variables
347 + *
348 + * @param string $body_processor Manually specifiy the method to use to process the body. Options are 'URLENCODED' and 'JSON'.
349 + *
350 + * @return array{string, scalar}[]
351 + */
352 + public function get_post_vars( string $body_processor = '' ) {
353 + $content_type = $this->get_header( 'content-type' );
354 +
355 + // If the body processor is specified by the rules file, trust it.
356 + if ( 'URLENCODED' === $body_processor ) {
357 + return $this->get_urlencoded_post_vars();
358 + }
359 + if ( 'JSON' === $body_processor ) {
360 + return $this->get_json_post_vars();
361 + }
362 +
363 + // Otherwise, use $_POST if it's not empty.
364 + if ( ! empty( $_POST ) ) {
365 + return flatten_array( $_POST );
366 + }
367 +
368 + // Lastly, try to parse the body based on the content type.
369 + if ( strpos( $content_type, 'application/json' ) !== false ) {
370 + return $this->get_json_post_vars();
371 + }
372 + if ( strpos( $content_type, 'application/x-www-form-urlencoded' ) !== false ) {
373 + return $this->get_urlencoded_post_vars();
374 + }
375 +
376 + // Don't try to parse any other content types.
377 + return array();
378 + }
379 +
380 + /**
381 + * Returns the files that were uploaded with this request (i.e. what's in the $_FILES superglobal)
382 + *
383 + * @return RequestFile[]
384 + */
385 + public function get_files() {
386 + $files = array();
387 + foreach ( $_FILES as $field_name => $arr ) {
388 + // flatten the values in case we were given inputs with brackets
389 + foreach ( flatten_array( $arr ) as list( $arr_key, $arr_value ) ) {
390 + if ( $arr_key === 'name' ) {
391 + // if this file was a simple (non-nested) name and unique, then just add it.
392 + $files[] = array(
393 + 'name' => $field_name,
394 + 'filename' => $arr_value,
395 + );
396 + } elseif ( 'name[' === substr( $arr_key, 0, 5 ) ) {
397 + // otherwise this was a file with a nested name and/or multiple files with the same name
398 + $files[] = array(
399 + 'name' => $field_name . substr( $arr_key, 4 ),
400 + 'filename' => $arr_value,
401 + );
402 + }
403 + }
404 + }
405 + return $files;
406 + }
407 +}