PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.2
Jetpack – WP Security, Backup, Speed, & Growth v16.2
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 13.7.2 13.8.3 All 506 releases
jetpack / jetpack_vendor / automattic / jetpack-waf / src / class-waf-request.php

class-waf-request.php in Jetpack – WP Security, Backup, Speed, & Growth 16.2, at jetpack_vendor/automattic/jetpack-waf/src/class-waf-request.php

408 lines 11.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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 }
408