PluginProbe ʕ •ᴥ•ʔ
Jetpack – WP Security, Backup, Speed, & Growth / 16.2-a.1
Jetpack – WP Security, Backup, Speed, & Growth v16.2-a.1
16.1.2 16.2-a.1 16.1.1 16.1 16.1-beta 16.1-beta.2 16.1-beta.3 16.1-a.5 16.1-a.3 16.0.1 16.1-a.1 16.0 16.0-beta 16.0-a.7 16.0-a.5 15.9.1 16.0-a.3 16.0-a.1 15.9 15.9-beta 15.9-a.7 15.9-a.5 15.9-a.3 15.9-a.1 15.8 15.8-beta 15.8-a.7 15.8-a.5 5.2.5 5.3.4 5.4.4 5.5.5 5.6.5 5.7.5 5.8.4 5.9.4 6.0.4 6.1 6.1.1 6.1.2 6.1.3 6.1.4 6.1.5 6.2 6.2.1 6.2.2 6.2.3 6.2.4 6.2.5 6.3 6.3.1 6.3.2 6.3.3 6.3.4 6.3.5 6.3.6 6.3.7 6.4 6.4.1 6.4.2 6.4.3 6.4.4 6.4.5 6.4.6 6.5 6.5.1 6.5.2 6.5.3 6.5.4 6.6 6.6.1 6.6.2 6.6.3 6.6.4 6.6.5 6.7 6.7.1 6.7.2 6.7.3 6.7.4 6.8 6.8.1 6.8.2 6.8.3 6.8.4 6.8.5 6.9 6.9.1 6.9.2 6.9.3 6.9.4 7.0 7.0.1 7.0.2 7.0.3 7.0.4 7.0.5 7.1 7.1.1 7.1.2 7.1.3 7.1.4 7.1.5 7.2 7.2.1 7.2.1.1 7.2.2 7.2.3 7.2.4 7.2.5 7.3 7.3.0.1 7.3.1 7.3.1.1 7.3.2 7.3.3 7.3.4 7.3.5 7.4 7.4.1 7.4.2 7.4.3 7.4.4 7.4.5 7.5 7.5.0.1 7.5.1 7.5.2 7.5.3 7.5.4 7.5.5 7.5.6 7.5.7 7.6 7.6.1 7.6.2 7.6.3 7.6.4 7.7 7.7.1 7.7.2 7.7.3 7.7.4 7.7.5 7.7.6 7.8 7.8.1 7.8.2 7.8.3 7.8.4 7.9 7.9.1 7.9.2 7.9.3 7.9.4 8.0 8.0.1 8.0.2 8.0.3 8.1 8.1.1 8.1.2 8.1.3 8.1.4 8.2 8.2.0.1 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.2.6 8.3 8.3.1 8.3.2 8.3.3 8.4 8.4.1 8.4.2 8.4.3 8.4.4 8.4.5 8.5 8.5.1 8.5.2 8.5.3 8.6 8.6.1 8.6.2 8.6.3 8.6.4 8.7 8.7.0.1 8.7.1 8.7.2 8.7.3 8.7.4 8.8 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.9 8.9.1 8.9.2 8.9.3 8.9.4 9.0 9.0.1 9.0.2 9.0.3 9.0.4 9.0.5 9.1 9.1.1 9.1.2 9.1.3 9.2 9.2.1 9.2.2 9.2.3 9.2.4 9.3 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.4 9.4.1 9.4.2 9.4.3 9.4.4 9.5 9.5.1 9.5.2 9.5.3 9.5.4 9.5.5 9.6 9.6.1 9.6.2 9.6.3 9.6.4 9.7 9.7.1 9.7.2 15.7-beta.2 9.7.3 15.7.1 9.8 15.8-a.1 9.8.1 15.8-a.3 9.8.2 2.0.9 9.8.3 2.1.7 9.9 2.2.10 9.9.1 2.3.10 9.9.2 2.4.7 9.9.3 2.5.5 2.6.6 2.7.5 2.8.5 2.9.6 3.0.6 3.1.5 3.2.5 3.3.6 3.4.6 3.5.6 3.6.4 3.7.5 3.8.5 3.9.10 4.0.7 4.1.4 4.2.5 4.3.5 4.4.5 4.5.3 4.6.3 4.7.4 4.8.5 4.9.3 5.0.3 5.1.4 trunk 10.0 10.0.1 10.0.2 10.1 10.1.1 10.1.2 10.2 10.2.1 10.2.2 10.2.3 10.3 10.3.1 10.3.2 10.4 10.4.1 10.4.2 10.5 10.5.1 10.5.2 10.5.3 10.6 10.6.1 10.6.2 10.7 10.7.1 10.7.2 10.8 10.8.1 10.8.2 10.9 10.9.1 10.9.2 10.9.3 11.0 11.0.1 11.0.2 11.1 11.1.1 11.1.2 11.1.3 11.1.4 11.2 11.2.1 11.2.2 11.3 11.3.1 11.3.2 11.3.3 11.3.4 11.4 11.4.1 11.4.2 11.5 11.5.1 11.5.2 11.5.3 11.6 11.6.1 11.6.2 11.7 11.7.1 11.7.2 11.7.3 11.8 11.8.3 11.8.4 11.8.5 11.8.6 11.9 11.9.1 11.9.2 11.9.3 12.0 12.0.1 12.0.2 12.1 12.1.1 12.1.2 12.2 12.2.1 12.2.2 12.3 12.3.1 12.4 12.4.1 12.5 12.5.1 12.6 12.6.1 12.6.2 12.6.3 12.7 12.7.1 12.7.2 12.8 12.8.1 12.8.2 12.9 12.9.1 12.9.2 12.9.3 12.9.4 13.0 13.0.1 13.1 13.1.1 13.1.2 13.1.3 13.1.4 13.2 13.2.1 13.2.2 13.2.3 13.3 13.3.1 13.3.2 13.4 13.4.1 13.4.2 13.4.3 13.4.4 13.5 13.5.1 13.6 13.6.1 13.7 13.7.1 13.8 13.8.1 13.8.2 13.9 13.9.1 14.0 14.1 14.2 14.2.1 14.3 14.4 14.4.1 14.5 14.6 14.7 14.8 14.9 14.9.1 15.0 15.0.1 15.0.2 15.1 15.1.1 15.2 15.3 15.3.1 15.4 15.5 15.6 15.7 15.7-a.1 15.7-a.3 15.7-a.5 15.7-a.7 15.7-beta
jetpack / jetpack_vendor / automattic / jetpack-waf / src / class-waf-runtime.php
jetpack / jetpack_vendor / automattic / jetpack-waf / src Last commit date
brute-force-protection 8 months ago exceptions 8 months ago abstract-blocked-login-page.php 2 months ago class-brute-force-protection.php 1 month ago class-compatibility.php 1 month ago class-rest-controller.php 8 months ago class-waf-blocked-login-page.php 8 months ago class-waf-blocklog-manager.php 8 months ago class-waf-cli.php 8 months ago class-waf-constants.php 1 year ago class-waf-initializer.php 1 month ago class-waf-operators.php 2 years ago class-waf-request.php 6 months ago class-waf-rules-manager.php 1 year ago class-waf-runner.php 1 month ago class-waf-runtime.php 2 months ago class-waf-standalone-bootstrap.php 1 month ago class-waf-stats.php 1 year ago class-waf-transforms.php 2 years ago functions.php 1 year ago
class-waf-runtime.php
861 lines
1 <?php
2 /**
3 * Runtime for Jetpack Waf
4 *
5 * @package automattic/jetpack-waf
6 */
7
8 namespace Automattic\Jetpack\Waf;
9
10 use Automattic\Jetpack\IP\Utils as IP_Utils;
11
12 require_once __DIR__ . '/functions.php';
13
14 // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- This class is all about sanitizing input.
15
16 /**
17 * The environment variable that defined the WAF running mode.
18 *
19 * @var string JETPACK_WAF_MODE
20 */
21
22 // Type aliases for this file.
23 <<<'PHAN'
24 @phan-type Target = array{ only?: string[], except?: string[], count?: boolean }
25 @phan-type TargetBag = array<string, Target>
26 PHAN;
27
28 /**
29 * Waf_Runtime class
30 */
31 class Waf_Runtime {
32 /**
33 * If used, normalize_array_targets() will just return the number of matching values, instead of the values themselves.
34 */
35 const NORMALIZE_ARRAY_COUNT = 1;
36 /**
37 * If used, normalize_array_targets() will apply "only" and "except" filters to the values of the source array, instead of the keys.
38 */
39 const NORMALIZE_ARRAY_MATCH_VALUES = 2;
40
41 /**
42 * The version of this runtime class. Used by rule files to ensure compatibility.
43 *
44 * @since 0.21.0
45 *
46 * @var int
47 */
48 public $version = 1;
49 /**
50 * Last rule.
51 *
52 * @var string
53 */
54 public $last_rule = '';
55 /**
56 * Matched vars.
57 *
58 * @var array
59 */
60 public $matched_vars = array();
61 /**
62 * Matched var.
63 *
64 * @var string
65 */
66 public $matched_var = '';
67 /**
68 * Matched var names.
69 *
70 * @var array
71 */
72 public $matched_vars_names = array();
73 /**
74 * Matched var name.
75 *
76 * @var string
77 */
78 public $matched_var_name = '';
79 /**
80 * Body Processor.
81 *
82 * @var string 'URLENCODED' | 'JSON' | ''
83 */
84 private $body_processor = '';
85
86 /**
87 * State.
88 *
89 * @var array
90 */
91 private $state = array();
92 /**
93 * Metadata.
94 *
95 * @var array
96 */
97 private $metadata = array();
98
99 /**
100 * Transforms.
101 *
102 * @var Waf_Transforms
103 */
104 private $transforms;
105 /**
106 * Operators.
107 *
108 * @var Waf_Operators
109 */
110 private $operators;
111
112 /**
113 * The request
114 *
115 * @var Waf_Request
116 */
117 private $request;
118
119 /**
120 * Rules to remove.
121 *
122 * @var array[]
123 */
124 private $rules_to_remove = array(
125 'id' => array(),
126 'tag' => array(),
127 );
128
129 /**
130 * Targets to remove.
131 *
132 * @var array[]
133 */
134 private $targets_to_remove = array(
135 'id' => array(),
136 'tag' => array(),
137 );
138
139 /**
140 * Constructor method.
141 *
142 * @param Waf_Transforms $transforms Transforms.
143 * @param Waf_Operators $operators Operators.
144 * @param ?Waf_Request $request Information about the request.
145 */
146 public function __construct( $transforms, $operators, $request = null ) {
147 $this->transforms = $transforms;
148 $this->operators = $operators;
149 $this->request = null === $request
150 ? new Waf_Request()
151 : $request;
152 }
153
154 /**
155 * Rule removed method.
156 *
157 * @param string $id Ids.
158 * @param string[] $tags Tags.
159 */
160 public function rule_removed( $id, $tags ) {
161 if ( isset( $this->rules_to_remove['id'][ $id ] ) ) {
162 return true;
163 }
164 foreach ( $tags as $tag ) {
165 if ( isset( $this->rules_to_remove['tag'][ $tag ] ) ) {
166 return true;
167 }
168 }
169 return false;
170 }
171
172 /**
173 * Update Targets.
174 *
175 * @param array $targets Targets.
176 * @param string $rule_id Rule id.
177 * @param string[] $rule_tags Rule tags.
178 */
179 public function update_targets( $targets, $rule_id, $rule_tags ) {
180 $updates = array();
181 // look for target updates based on the rule's ID.
182 if ( isset( $this->targets_to_remove['id'][ $rule_id ] ) ) {
183 foreach ( $this->targets_to_remove['id'][ $rule_id ] as $name => $props ) {
184 $updates[] = array( $name, $props );
185 }
186 }
187 // look for target updates based on the rule's tags.
188 foreach ( $rule_tags as $tag ) {
189 if ( isset( $this->targets_to_remove['tag'][ $tag ] ) ) {
190 foreach ( $this->targets_to_remove['tag'][ $tag ] as $name => $props ) {
191 $updates[] = array( $name, $props );
192 }
193 }
194 }
195 // apply any found target updates.
196
197 foreach ( $updates as list( $name, $props ) ) {
198 if ( isset( $targets[ $name ] ) ) {
199 // we only need to remove targets that exist.
200 if ( true === $props ) {
201 // if the entire target is being removed, remove it.
202 unset( $targets[ $name ] );
203 } else {
204 // otherwise just mark single props to ignore.
205 $targets[ $name ]['except'] = array_merge(
206 $targets[ $name ]['except'] ?? array(),
207 $props
208 );
209 }
210 }
211 }
212 return $targets;
213 }
214
215 /**
216 * Return TRUE if at least one of the targets matches the rule.
217 *
218 * @param string[] $transforms One of the transform methods defined in the Jetpack Waf_Transforms class.
219 * @param TargetBag $targets Targets.
220 * @param string $match_operator Match operator.
221 * @param mixed $match_value Match value.
222 * @param bool $match_not Match not.
223 * @param bool $capture Capture.
224 * @return bool
225 */
226 public function match_targets( $transforms, $targets, $match_operator, $match_value, $match_not, $capture = false ) {
227 $match_found = false;
228
229 // get values.
230 $values = $this->normalize_targets( $targets );
231
232 // apply transforms.
233 foreach ( $transforms as $t ) {
234 foreach ( $values as &$v ) {
235 $v['value'] = $this->transforms->$t( $v['value'] );
236 }
237 }
238 unset( $v );
239 // pass each target value to the operator to find any that match.
240 $matched = array();
241 $captures = array();
242 foreach ( $values as $v ) {
243 $match = $this->operators->{$match_operator}( $v['value'], $match_value );
244 $did_match = false !== $match;
245 if ( $match_not !== $did_match ) {
246 // If either:
247 // - rule is negated ("not" flag set) and the target was not matched
248 // - rule not negated and the target was matched
249 // then this is considered a match.
250 $match_found = true;
251 $this->matched_vars_names[] = $v['name'];
252 $this->matched_vars[] = $v['value'];
253 $this->matched_var_name = end( $this->matched_vars_names );
254 $this->matched_var = end( $this->matched_vars );
255 $matched[] = array( $v, $match );
256 // Set any captured matches into state if the rule has the "capture" flag.
257 if ( $capture ) {
258 $captures = is_array( $match ) ? $match : array( $match );
259 foreach ( array_slice( $captures, 0, 10 ) as $i => $c ) {
260 $this->set_var( "tx.$i", $c );
261 }
262 }
263 }
264 }
265
266 return $match_found;
267 }
268
269 /**
270 * Generate a secure hash for an IP address.
271 *
272 * @param string $ip IP address.
273 * @return string Hashed IP.
274 */
275 private function get_ip_hash( string $ip ): string {
276 $hash_key = wp_salt( 'auth' );
277 return hash_hmac( 'sha256', $ip, $hash_key );
278 }
279
280 /**
281 * Check if the IP is allowed for recovery.
282 *
283 * @param string $ip IP address.
284 * @return bool
285 */
286 public function is_ip_allowed_for_recovery( string $ip ): bool {
287 $allow_hash = get_transient( 'jetpack_waf_recovery_' . $ip );
288 return $allow_hash && hash_equals( $allow_hash, $this->get_ip_hash( $ip ) );
289 }
290
291 /**
292 * Process a recovery attempt.
293 *
294 * @param string $real_ip The real IP address of the request.
295 */
296 private function allow_login_or_prompt_recovery( $real_ip ) {
297 $blocked_login_page = Waf_Blocked_Login_Page::instance( $real_ip );
298
299 if ( $blocked_login_page->is_blocked_user_valid() ) {
300 // Allow the IP to bypass the block for 15 minutes.
301 set_transient( 'jetpack_waf_recovery_' . $real_ip, $this->get_ip_hash( $real_ip ), 15 * 60 );
302 return;
303 }
304
305 $blocked_login_page->render_and_die();
306 }
307
308 /**
309 * Block.
310 *
311 * @param string $action Action.
312 * @param string $rule_id Rule id.
313 * @param string $reason Block reason.
314 * @param int $status_code Http status code.
315 */
316 public function block( $action, $rule_id, $reason, $status_code = 403 ) {
317 if ( 'ip block list' === $reason ) {
318 $real_ip = $this->request->get_real_user_ip_address();
319
320 if ( $this->is_ip_allowed_for_recovery( $real_ip ) ) {
321 return;
322 }
323
324 global $pagenow;
325 if ( isset( $pagenow ) && 'wp-login.php' === $pagenow ) {
326 $this->allow_login_or_prompt_recovery( $real_ip );
327 return;
328 }
329 }
330
331 if ( ! $reason ) {
332 $reason = "rule $rule_id";
333 } else {
334 $reason = $this->sanitize_output( $reason );
335 }
336
337 Waf_Blocklog_Manager::write_blocklog( $rule_id, $reason );
338 error_log( "Jetpack WAF Blocked Request\t$action\t$rule_id\t$status_code\t$reason" );
339 header( "X-JetpackWAF-Blocked: $status_code - rule $rule_id" );
340 if ( defined( 'JETPACK_WAF_MODE' ) && 'normal' === JETPACK_WAF_MODE ) {
341 $protocol = isset( $_SERVER['SERVER_PROTOCOL'] ) ? wp_unslash( $_SERVER['SERVER_PROTOCOL'] ) : 'HTTP';
342 header( $protocol . ' 403 Forbidden', true, $status_code );
343 die( "rule $rule_id - reason $reason" );
344 }
345 }
346
347 /**
348 * Redirect.
349 *
350 * @param string $rule_id Rule id.
351 * @param string $url Url.
352 * @return never
353 */
354 public function redirect( $rule_id, $url ) {
355 error_log( "Jetpack WAF Redirected Request.\tRule:$rule_id\t$url" );
356 header( "Location: $url" );
357 exit( 0 );
358 }
359
360 /**
361 * Flag rule for removal.
362 *
363 * @param string $prop Prop.
364 * @param string $value Value.
365 */
366 public function flag_rule_for_removal( $prop, $value ) {
367 if ( 'id' === $prop ) {
368 $this->rules_to_remove['id'][ $value ] = true;
369 } else {
370 $this->rules_to_remove['tag'][ $value ] = true;
371 }
372 }
373
374 /**
375 * Flag target for removal.
376 *
377 * @param string $id_or_tag Id or tag.
378 * @param string $id_or_tag_value Id or tag value.
379 * @param string $name Name.
380 * @param string $prop Prop.
381 */
382 public function flag_target_for_removal( $id_or_tag, $id_or_tag_value, $name, $prop = null ) {
383 if ( null === $prop ) {
384 $this->targets_to_remove[ $id_or_tag ][ $id_or_tag_value ][ $name ] = true;
385 } elseif (
386 ! isset( $this->targets_to_remove[ $id_or_tag ][ $id_or_tag_value ][ $name ] )
387 // if the entire target is already being removed then it would be redundant to remove a single property.
388 || true !== $this->targets_to_remove[ $id_or_tag ][ $id_or_tag_value ][ $name ]
389 ) {
390 $this->targets_to_remove[ $id_or_tag ][ $id_or_tag_value ][ $name ][] = $prop;
391 }
392 }
393
394 /**
395 * Get variable value.
396 *
397 * @param string $key Key.
398 */
399 public function get_var( $key ) {
400 return $this->state[ $key ] ?? '';
401 }
402
403 /**
404 * Set variable value.
405 *
406 * @param string $key Key.
407 * @param string $value Value.
408 */
409 public function set_var( $key, $value ) {
410 $this->state[ $key ] = $value;
411 }
412
413 /**
414 * Increment variable.
415 *
416 * @param string $key Key.
417 * @param mixed $value Value.
418 */
419 public function inc_var( $key, $value ) {
420 if ( ! isset( $this->state[ $key ] ) ) {
421 $this->state[ $key ] = 0;
422 }
423 $this->state[ $key ] += floatval( $value );
424 }
425
426 /**
427 * Decrement variable.
428 *
429 * @param string $key Key.
430 * @param mixed $value Value.
431 */
432 public function dec_var( $key, $value ) {
433 if ( ! isset( $this->state[ $key ] ) ) {
434 $this->state[ $key ] = 0;
435 }
436 $this->state[ $key ] -= floatval( $value );
437 }
438
439 /**
440 * Unset variable.
441 *
442 * @param string $key Key.
443 */
444 public function unset_var( $key ) {
445 unset( $this->state[ $key ] );
446 }
447
448 /**
449 * A cache of metadata about the incoming request.
450 *
451 * @param string $key The type of metadata to request ('headers', 'request_method', etc.).
452 */
453 public function meta( $key ) {
454 if ( ! isset( $this->metadata[ $key ] ) ) {
455 $value = null;
456 switch ( $key ) {
457 case 'headers':
458 $value = $this->request->get_headers();
459 break;
460 case 'headers_names':
461 $value = $this->args_names( $this->meta( 'headers' ) );
462 break;
463 case 'request_method':
464 $value = $this->request->get_method();
465 break;
466 case 'request_protocol':
467 $value = $this->request->get_protocol();
468 break;
469 case 'request_uri':
470 $value = $this->request->get_uri( false );
471 break;
472 case 'request_uri_raw':
473 $value = $this->request->get_uri( true );
474 break;
475 case 'request_filename':
476 $value = $this->request->get_filename();
477 break;
478 case 'request_line':
479 $value = sprintf(
480 '%s %s %s',
481 $this->request->get_method(),
482 $this->request->get_uri( false ),
483 $this->request->get_protocol()
484 );
485 break;
486 case 'request_basename':
487 $value = $this->request->get_basename();
488 break;
489 case 'request_body':
490 $value = $this->request->get_body();
491 break;
492 case 'query_string':
493 $value = $this->request->get_query_string();
494 break;
495 case 'args_get':
496 $value = $this->request->get_get_vars();
497 break;
498 case 'args_get_names':
499 $value = $this->args_names( $this->meta( 'args_get' ) );
500 break;
501 case 'args_post':
502 $value = $this->request->get_post_vars( $this->get_body_processor() );
503 break;
504 case 'args_post_names':
505 $value = $this->args_names( $this->meta( 'args_post' ) );
506 break;
507 case 'args':
508 $value = array_merge( $this->meta( 'args_get' ), $this->meta( 'args_post' ) );
509 break;
510 case 'args_names':
511 $value = $this->args_names( $this->meta( 'args' ) );
512 break;
513 case 'request_cookies':
514 $value = $this->request->get_cookies();
515 break;
516 case 'request_cookies_names':
517 $value = $this->args_names( $this->meta( 'request_cookies' ) );
518 break;
519 case 'files':
520 $value = array();
521 foreach ( $this->request->get_files() as $f ) {
522 $value[] = array( $f['name'], $f['filename'] );
523 }
524 break;
525 case 'files_names':
526 $value = $this->args_names( $this->meta( 'files' ) );
527 break;
528 case 'matched_vars':
529 $value = array_combine( $this->matched_vars_names, $this->matched_vars );
530 break;
531 case 'matched_var':
532 $value = array( $this->matched_var_name => $this->matched_var );
533 break;
534 case 'matched_vars_names':
535 $value = $this->matched_vars_names;
536 break;
537 case 'matched_var_name':
538 $value = array( $this->matched_var_name );
539 break;
540 }
541 $this->metadata[ $key ] = $value;
542 }
543
544 return $this->metadata[ $key ];
545 }
546
547 /**
548 * State values.
549 *
550 * @param string $prefix Prefix.
551 */
552 private function state_values( $prefix ) {
553 $output = array();
554 $len = strlen( $prefix );
555 foreach ( $this->state as $k => $v ) {
556 if ( 0 === stripos( $k, $prefix ) ) {
557 $output[ substr( $k, $len ) ] = $v;
558 }
559 }
560
561 return $output;
562 }
563
564 /**
565 * Get the body processor.
566 *
567 * @return string
568 */
569 private function get_body_processor() {
570 return $this->body_processor;
571 }
572
573 /**
574 * Set the body processor.
575 *
576 * @param string $processor Processor to set. Either 'URLENCODED' or 'JSON'.
577 *
578 * @return void
579 */
580 public function set_body_processor( $processor ) {
581 if ( $processor === 'URLENCODED' || $processor === 'JSON' ) {
582 $this->body_processor = $processor;
583 }
584 }
585
586 /**
587 * Change a string to all lowercase and replace spaces and underscores with dashes.
588 *
589 * @param string $name Name.
590 * @return string
591 */
592 public function normalize_header_name( $name ) {
593 return str_replace( array( ' ', '_' ), '-', strtolower( $name ) );
594 }
595
596 /**
597 * Get match-able values from a collection of targets.
598 *
599 * This function expects an associative array of target items, and returns an array of possible values from those targets that can be used to match against.
600 * The key is the lowercase target name (i.e. `args`, `request_headers`, etc) - see https://github.com/SpiderLabs/ModSecurity/wiki/Reference-Manual-(v3.x)#Variables
601 * The value is an associative array of options that define how to narrow down the returned values for that target if it's an array (ARGS, for example). The possible options are:
602 * count: If `true`, then the returned value will a count of how many matched targets were found, rather then the actual values of those targets.
603 * For example, &ARGS_GET will return the number of keys the query string.
604 * only: If specified, then only values in that target that match the given key will be returned.
605 * For example, ARGS_GET:id|ARGS_GET:/^name/ will only return the values for `$_GET['id']` and any key in `$_GET` that starts with `name`
606 * except: If specified, then values in that target will be left out from the returned values (even if they were included in an `only` option)
607 * For example, ARGS_GET|!ARGS_GET:z will return every value from `$_GET` except for `$_GET['z']`.
608 *
609 * This function will return an array of associative arrays. Each with:
610 * name: The target name that this value came from (i.e. the key in the input `$targets` argument )
611 * source: For targets that are associative arrays (like ARGS), this will be the target name AND the key in that target (i.e. "args:z" for ARGS:z)
612 * value: The value that was found in the associated target.
613 *
614 * @param TargetBag $targets An assoc. array with keys that are target name(s) and values are options for how to process that target (include/exclude rules, whether to return values or counts).
615 * @return array{name: string, source: string, value: mixed}[]
616 */
617 public function normalize_targets( $targets ) {
618 $return = array();
619 foreach ( $targets as $k => $v ) {
620 $count_only = isset( $v['count'] ) ? self::NORMALIZE_ARRAY_COUNT : 0;
621 $only = $v['only'] ?? array();
622 $except = $v['except'] ?? array();
623 $_k = strtolower( $k );
624 switch ( $_k ) {
625 case 'request_headers':
626 $this->normalize_array_target(
627 // get the headers that came in with this request
628 $this->meta( 'headers' ),
629 // ensure only and exclude filters are normalized
630 array_map( array( $this->request, 'normalize_header_name' ), $only ),
631 array_map( array( $this->request, 'normalize_header_name' ), $except ),
632 $k,
633 $return,
634 // flags
635 $count_only
636 );
637 continue 2;
638 case 'request_headers_names':
639 $this->normalize_array_target( $this->meta( 'headers_names' ), $only, $except, $k, $return, $count_only | self::NORMALIZE_ARRAY_MATCH_VALUES );
640 continue 2;
641 case 'request_method':
642 case 'request_protocol':
643 case 'request_uri':
644 case 'request_uri_raw':
645 case 'request_filename':
646 case 'request_basename':
647 case 'request_body':
648 case 'query_string':
649 case 'request_line':
650 $v = $this->meta( $_k );
651 break;
652 case 'tx':
653 case 'ip':
654 $this->normalize_array_target( $this->state_values( "$k." ), $only, $except, $k, $return, $count_only );
655 continue 2;
656 case 'request_cookies':
657 case 'args':
658 case 'args_get':
659 case 'args_post':
660 case 'files':
661 $this->normalize_array_target( $this->meta( $_k ), $only, $except, $k, $return, $count_only );
662 continue 2;
663 case 'request_cookies_names':
664 case 'args_names':
665 case 'args_get_names':
666 case 'args_post_names':
667 case 'files_names':
668 // get the "full" data (for 'args_names' get data for 'args') and stripe it down to just the key names
669 $data = array_map(
670 function ( $item ) {
671 return $item[0]; },
672 $this->meta( substr( $_k, 0, -6 ) )
673 );
674 $this->normalize_array_target( $data, $only, $except, $k, $return, $count_only | self::NORMALIZE_ARRAY_MATCH_VALUES );
675 continue 2;
676 case 'matched_var':
677 $this->normalize_array_target( $this->meta( $k ), $only, $except, $k, $return, $count_only );
678 continue 2;
679
680 case 'matched_var_name':
681 $this->normalize_array_target( $this->meta( $k ), $only, $except, $k, $return, $count_only | self::NORMALIZE_ARRAY_MATCH_VALUES );
682 continue 2;
683
684 case 'matched_vars':
685 $this->normalize_array_target( $this->meta( $k ), $only, $except, $k, $return, $count_only );
686 continue 2;
687
688 case 'matched_vars_names':
689 $this->normalize_array_target( $this->meta( $k ), $only, $except, $k, $return, $count_only | self::NORMALIZE_ARRAY_MATCH_VALUES );
690 continue 2;
691
692 default:
693 var_dump( 'Unknown target', $k, $v );
694 exit( 0 );
695 }
696 $return[] = array(
697 'name' => $k,
698 'value' => $v,
699 'source' => $k,
700 );
701 }
702
703 return $return;
704 }
705
706 /**
707 * Reset matched vars after processing a rule.
708 *
709 * @return void
710 */
711 public function reset_matched_vars() {
712 $this->matched_vars = array();
713 $this->matched_vars_names = array();
714 $this->matched_var = '';
715 $this->matched_var_name = '';
716 unset(
717 $this->metadata['matched_var'],
718 $this->metadata['matched_vars'],
719 $this->metadata['matched_vars_names'],
720 $this->metadata['matched_var_name']
721 );
722 }
723
724 /**
725 * Verifies if the IP from the current request is in an array.
726 *
727 * @param array $array Array of IP addresses to verify the request IP against.
728 * @return bool
729 */
730 public function is_ip_in_array( $array ) {
731 $real_ip = $this->request->get_real_user_ip_address();
732 $array_length = count( $array );
733
734 for ( $i = 0; $i < $array_length; $i++ ) {
735 // Check if the IP matches a provided range or CIDR notation.
736 $range = strpos( $array[ $i ], '/' ) !== false ? array( $array[ $i ], null ) : explode( '-', $array[ $i ] );
737 if ( count( $range ) === 2 ) {
738 if ( IP_Utils::ip_address_is_in_range( $real_ip, $range[0], $range[1] ) ) {
739 return true;
740 }
741 continue;
742 }
743
744 // Check if the IP is an exact match.
745 if ( $real_ip === $array[ $i ] ) {
746 return true;
747 }
748 }
749
750 return false;
751 }
752
753 /**
754 * Extract values from an associative array, potentially applying filters and/or counting results.
755 *
756 * @param array{0: string, 1: scalar}|scalar[] $source The source assoc. array of values (i.e. $_GET, $_SERVER, etc.).
757 * @param string[] $only Only include the values for these keys in the output.
758 * @param string[] $excl Never include the values for these keys in the output.
759 * @param string $name The name of this target (see https://github.com/SpiderLabs/ModSecurity/wiki/Reference-Manual-(v3.x)#Variables).
760 * @param array $results Array to add output values to, will be modified by this method.
761 * @param int $flags Any of the NORMALIZE_ARRAY_* constants defined at the top of the class.
762 */
763 private function normalize_array_target( $source, $only, $excl, $name, &$results, $flags = 0 ) {
764 $output = array();
765 $has_only = isset( $only[0] );
766 $has_excl = isset( $excl[0] );
767
768 foreach ( $source as $source_key => $source_val ) {
769 if ( is_array( $source_val ) ) {
770 // if $source_val looks like a tuple from flatten_array(), then use the tuple as the key and value
771 $source_key = $source_val[0];
772 $source_val = $source_val[1];
773 }
774 $filter_match = ( $flags & self::NORMALIZE_ARRAY_MATCH_VALUES ) > 0 ? $source_val : $source_key;
775 // if this key is on the "exclude" list, skip it
776 if ( $has_excl && $this->key_matches( $filter_match, $excl ) ) {
777 continue;
778 }
779 // if this key isn't in our "only" list, then skip it
780 if ( $has_only && ! $this->key_matches( $filter_match, $only ) ) {
781 continue;
782 }
783 // otherwise add this key/value to our output
784 $output[] = array( $source_key, $source_val );
785 }
786
787 if ( ( $flags & self::NORMALIZE_ARRAY_COUNT ) > 0 ) {
788 // If we've been told to just count the values, then just count them.
789 $results[] = array(
790 'name' => (string) $name,
791 'value' => count( $output ),
792 'source' => '&' . $name,
793 );
794 } else {
795 foreach ( $output as list( $item_name, $item_value ) ) {
796 $results[] = array(
797 'name' => (string) $item_name,
798 'value' => $item_value,
799 'source' => "$name:$item_name",
800 );
801 }
802 }
803
804 return $results;
805 }
806
807 /**
808 * Given an array of tuples - probably from flatten_array() - return a new array
809 * consisting of only the first value (the key name) from each tuple.
810 *
811 * @param array{0:string, 1:scalar}[] $flat_array An array of tuples.
812 * @return string[]
813 */
814 private function args_names( $flat_array ) {
815 $names = array_map(
816 function ( $tuple ) {
817 return $tuple[0];
818 },
819 $flat_array
820 );
821 return array_unique( $names );
822 }
823
824 /**
825 * Return whether or not a given $input key matches one of the given $patterns.
826 *
827 * @param string $input Key name to test against patterns.
828 * @param string[] $patterns Patterns to test key name with.
829 * @return bool
830 */
831 private function key_matches( $input, $patterns ) {
832 foreach ( $patterns as $p ) {
833 if ( '/' === $p[0] ) {
834 if ( 1 === preg_match( $p, $input ) ) {
835 return true;
836 }
837 } elseif ( 0 === strcasecmp( $p, $input ) ) {
838 return true;
839 }
840 }
841
842 return false;
843 }
844
845 /**
846 * Sanitize output generated from the request that was blocked.
847 *
848 * @param string $output Output to sanitize.
849 */
850 public function sanitize_output( $output ) {
851 $url_decoded_output = rawurldecode( $output );
852 $html_entities_output = htmlentities( $url_decoded_output, ENT_QUOTES, 'UTF-8' );
853 // @phpcs:disable Squiz.Strings.DoubleQuoteUsage.NotRequired
854 $escapers = array( "\\", "/", "\"", "\n", "\r", "\t", "\x08", "\x0c" );
855 $replacements = array( "\\\\", "\\/", "\\\"", "\\n", "\\r", "\\t", "\\f", "\\b" );
856 // @phpcs:enable Squiz.Strings.DoubleQuoteUsage.NotRequired
857
858 return( str_replace( $escapers, $replacements, $html_entities_output ) );
859 }
860 }
861