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
jetpack / jetpack_vendor / automattic / jetpack-waf / src / class-waf-blocklog-manager.php

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

442 lines 12.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Blocklog manager for the WAF
4 *
5 * @package automattic/jetpack-waf
6 */
7
8 namespace Automattic\Jetpack\Waf;
9
10 /**
11 * Class used to manage blocklog operations
12 */
13 class Waf_Blocklog_Manager {
14
15 const BLOCKLOG_OPTION_NAME_DAILY_SUMMARY = 'jetpack_waf_blocklog_daily_summary';
16 const BLOCKLOG_OPTION_NAME_ALL_TIME_BLOCK_COUNT = 'jetpack_waf_all_time_block_count';
17
18 /**
19 * Database connection.
20 *
21 * @var \mysqli|null
22 */
23 private static $db_connection = null;
24
25 /**
26 * Gets the path to the waf-blocklog file.
27 *
28 * @return string The waf-blocklog file path.
29 */
30 public static function get_blocklog_file_path() {
31 return trailingslashit( JETPACK_WAF_DIR ) . 'waf-blocklog';
32 }
33
34 /**
35 * Connect to WordPress database.
36 *
37 * @return \mysqli|null
38 */
39 private static function connect_to_wordpress_db() {
40 if ( self::$db_connection !== null ) {
41 return self::$db_connection;
42 }
43
44 if ( ! file_exists( JETPACK_WAF_WPCONFIG ) ) {
45 return null;
46 }
47
48 require_once JETPACK_WAF_WPCONFIG;
49 // @phan-suppress-next-line PhanUndeclaredConstant - These constants are defined in the wp-config file.
50 $conn = new \mysqli( DB_HOST, DB_USER, DB_PASSWORD, DB_NAME ); // phpcs:ignore WordPress.DB.RestrictedClasses.mysql__mysqli
51
52 if ( $conn->connect_error ) {
53 error_log( 'Could not connect to the database:' . $conn->connect_error );
54 return null;
55 }
56
57 self::$db_connection = $conn;
58 return self::$db_connection;
59 }
60
61 /**
62 * Close the database connection.
63 *
64 * @return void
65 */
66 private static function close_db_connection() {
67 if ( self::$db_connection ) {
68 self::$db_connection->close();
69 self::$db_connection = null;
70 }
71 }
72
73 /**
74 * Serialize a value for storage in a WordPress option.
75 *
76 * @param mixed $value The value to serialize.
77 * @return string The serialized value.
78 */
79 private static function serialize_option_value( $value ) {
80 return serialize( $value );
81 }
82
83 /**
84 * Unserialize a value from a WordPress option.
85 *
86 * @param string $value The serialized value.
87 * @return mixed The unserialized value.
88 */
89 private static function unserialize_option_value( string $value ) {
90 return unserialize( $value );
91 }
92
93 /**
94 * Create the log table when plugin is activated.
95 *
96 * @return void
97 */
98 public static function create_blocklog_table() {
99 global $wpdb;
100
101 require_once ABSPATH . 'wp-admin/includes/upgrade.php';
102
103 $sql = "
104 CREATE TABLE {$wpdb->prefix}jetpack_waf_blocklog (
105 log_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
106 timestamp datetime NOT NULL,
107 rule_id BIGINT NOT NULL,
108 reason longtext NOT NULL,
109 PRIMARY KEY (log_id),
110 KEY timestamp (timestamp)
111 )
112 ";
113
114 dbDelta( $sql );
115 }
116
117 /**
118 * Write block logs to database.
119 *
120 * @param array $log_data Log data.
121 *
122 * @return void
123 */
124 private static function write_blocklog_row( $log_data ) {
125 $conn = self::connect_to_wordpress_db();
126
127 if ( ! $conn ) {
128 return;
129 }
130
131 global $table_prefix;
132
133 $statement = $conn->prepare( "INSERT INTO {$table_prefix}jetpack_waf_blocklog(reason,rule_id, timestamp) VALUES (?, ?, ?)" );
134 if ( false !== $statement ) {
135 $statement->bind_param( 'sis', $log_data['reason'], $log_data['rule_id'], $log_data['timestamp'] );
136 $statement->execute();
137
138 if ( $conn->insert_id > 100 ) {
139 $conn->query( "DELETE FROM {$table_prefix}jetpack_waf_blocklog ORDER BY log_id LIMIT 1" );
140 }
141 }
142 }
143
144 /**
145 * Get the daily summary stats from the database.
146 *
147 * @return array The daily summary stats.
148 */
149 private static function get_daily_summary() {
150 global $table_prefix;
151 $db_connection = self::connect_to_wordpress_db();
152 if ( ! $db_connection ) {
153 return array();
154 }
155
156 $result = $db_connection->query( "SELECT option_value FROM {$table_prefix}options WHERE option_name = '" . self::BLOCKLOG_OPTION_NAME_DAILY_SUMMARY . "'" );
157 if ( ! $result ) {
158 return array();
159 }
160
161 $row = $result->fetch_assoc();
162 if ( ! $row ) {
163 return array();
164 }
165
166 $daily_summary = self::unserialize_option_value( $row['option_value'] );
167 $result->free();
168
169 return is_array( $daily_summary ) ? $daily_summary : array();
170 }
171
172 /**
173 * Increments the current date's daily summary stat.
174 *
175 * @param array $current_value The current value of the daily summary.
176 *
177 * @return array The updated daily summary.
178 */
179 public static function increment_daily_summary( array $current_value ) {
180 $date = gmdate( 'Y-m-d' );
181 $value = intval( $current_value[ $date ] ?? 0 );
182 $current_value[ $date ] = $value + 1;
183
184 return $current_value;
185 }
186
187 /**
188 * Update the daily summary option in the database.
189 *
190 * @param array $value The value to update.
191 *
192 * @return void
193 */
194 private static function write_daily_summary_row( array $value ) {
195 global $table_prefix;
196 $option_name = self::BLOCKLOG_OPTION_NAME_DAILY_SUMMARY;
197
198 $db_connection = self::connect_to_wordpress_db();
199 if ( ! $db_connection ) {
200 return;
201 }
202
203 $updated_value = self::serialize_option_value( $value );
204
205 $statement = $db_connection->prepare( "INSERT INTO {$table_prefix}options (option_name, option_value) VALUES (?, ?) ON DUPLICATE KEY UPDATE option_value = ?" );
206 if ( false !== $statement ) {
207 $statement->bind_param( 'sss', $option_name, $updated_value, $updated_value );
208 $statement->execute();
209 }
210 }
211
212 /**
213 * Update the daily summary stats for the current date.
214 *
215 * @return void
216 */
217 private static function write_daily_summary() {
218 $stats = self::get_daily_summary();
219 $stats = self::increment_daily_summary( $stats );
220 $stats = self::filter_last_30_days( $stats );
221
222 self::write_daily_summary_row( $stats );
223 }
224
225 /**
226 * Get the all-time block count value from the database.
227 *
228 * @return int The all-time block count.
229 */
230 private static function get_all_time_block_count_value() {
231 global $table_prefix;
232 $db_connection = self::connect_to_wordpress_db();
233 if ( ! $db_connection ) {
234 return 0;
235 }
236
237 $result = $db_connection->query( "SELECT option_value FROM {$table_prefix}options WHERE option_name = '" . self::BLOCKLOG_OPTION_NAME_ALL_TIME_BLOCK_COUNT . "'" );
238 if ( ! $result ) {
239 return 0;
240 }
241
242 $row = $result->fetch_assoc();
243 if ( ! $row ) {
244 return 0;
245 }
246
247 $all_time_block_count = intval( $row['option_value'] );
248 $result->free();
249
250 return $all_time_block_count;
251 }
252
253 /**
254 * Update the all-time block count value in the database.
255 *
256 * @param int $value The value to update.
257 * @return void
258 */
259 private static function write_all_time_block_count_row( int $value ) {
260 global $table_prefix;
261 $option_name = self::BLOCKLOG_OPTION_NAME_ALL_TIME_BLOCK_COUNT;
262
263 $db_connection = self::connect_to_wordpress_db();
264 if ( ! $db_connection ) {
265 return;
266 }
267
268 $statement = $db_connection->prepare( "INSERT INTO {$table_prefix}options (option_name, option_value) VALUES (?, ?) ON DUPLICATE KEY UPDATE option_value = ?" );
269 if ( false !== $statement ) {
270 $statement->bind_param( 'sii', $option_name, $value, $value );
271 $statement->execute();
272 }
273 }
274
275 /**
276 * Increment the all-time stats.
277 *
278 * @return void
279 */
280 private static function write_all_time_block_count() {
281 $block_count = self::get_all_time_block_count_value();
282 if ( ! $block_count ) {
283 $block_count = self::get_default_all_time_stat_value();
284 }
285
286 self::write_all_time_block_count_row( $block_count + 1 );
287 }
288
289 /**
290 * Filters the stats to retain only data for the last 30 days.
291 *
292 * @param array $stats The array of stats to prune.
293 *
294 * @return array Pruned stats array.
295 */
296 public static function filter_last_30_days( array $stats ) {
297 $today = gmdate( 'Y-m-d' );
298 $one_month_ago = gmdate( 'Y-m-d', strtotime( '-30 days' ) );
299
300 return array_filter(
301 $stats,
302 function ( $date ) use ( $one_month_ago, $today ) {
303 return $date >= $one_month_ago && $date <= $today;
304 },
305 ARRAY_FILTER_USE_KEY
306 );
307 }
308
309 /**
310 * Get the total number of blocked requests for today.
311 *
312 * @return int
313 */
314 public static function get_current_day_block_count() {
315 $stats = get_option( self::BLOCKLOG_OPTION_NAME_DAILY_SUMMARY, array() );
316 $today = gmdate( 'Y-m-d' );
317
318 return $stats[ $today ] ?? 0;
319 }
320
321 /**
322 * Get the total number of blocked requests for last thirty days.
323 *
324 * @return int
325 */
326 public static function get_thirty_days_block_counts() {
327 $stats = get_option( self::BLOCKLOG_OPTION_NAME_DAILY_SUMMARY, array() );
328 $total_blocks = 0;
329
330 foreach ( $stats as $count ) {
331 $total_blocks += intval( $count );
332 }
333
334 return $total_blocks;
335 }
336
337 /**
338 * Get the total number of blocked requests for all time.
339 *
340 * @return int
341 */
342 public static function get_all_time_block_count() {
343 $all_time_block_count = get_option( self::BLOCKLOG_OPTION_NAME_ALL_TIME_BLOCK_COUNT, false );
344
345 if ( false !== $all_time_block_count ) {
346 return intval( $all_time_block_count );
347 }
348
349 return self::get_default_all_time_stat_value();
350 }
351
352 /**
353 * Compute the initial all-time stats value.
354 *
355 * @return int The initial all-time stats value.
356 */
357 private static function get_default_all_time_stat_value() {
358 $conn = self::connect_to_wordpress_db();
359 if ( ! $conn ) {
360 return 0;
361 }
362
363 global $table_prefix;
364
365 $last_log_id_result = $conn->query( "SELECT log_id FROM {$table_prefix}jetpack_waf_blocklog ORDER BY log_id DESC LIMIT 1" );
366
367 $all_time_block_count = 0;
368
369 if ( $last_log_id_result && $last_log_id_result->num_rows > 0 ) {
370 $row = $last_log_id_result->fetch_assoc();
371 if ( $row !== null && isset( $row['log_id'] ) ) {
372 $all_time_block_count = $row['log_id'];
373 }
374 }
375
376 return intval( $all_time_block_count );
377 }
378
379 /**
380 * Get the headers for logging purposes.
381 *
382 * @return array The headers.
383 */
384 public static function get_request_headers() {
385 $all_headers = getallheaders();
386 $exclude_headers = array( 'Authorization', 'Cookie', 'Proxy-Authorization', 'Set-Cookie' );
387
388 foreach ( $exclude_headers as $header ) {
389 unset( $all_headers[ $header ] );
390 }
391
392 return $all_headers;
393 }
394
395 /**
396 * Write block logs. We won't write to the file if it exceeds 100 mb.
397 *
398 * @param string $rule_id The rule ID that triggered the block.
399 * @param string $reason The reason for the block.
400 *
401 * @return void
402 */
403 public static function write_blocklog( $rule_id, $reason ) {
404 $log_data = array();
405 $log_data['rule_id'] = $rule_id;
406 $log_data['reason'] = $reason;
407 $log_data['timestamp'] = gmdate( 'Y-m-d H:i:s' );
408 $log_data['request_uri'] = isset( $_SERVER['REQUEST_URI'] ) ? \stripslashes( $_SERVER['REQUEST_URI'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
409 $log_data['user_agent'] = isset( $_SERVER['HTTP_USER_AGENT'] ) ? \stripslashes( $_SERVER['HTTP_USER_AGENT'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
410 $log_data['referer'] = isset( $_SERVER['HTTP_REFERER'] ) ? \stripslashes( $_SERVER['HTTP_REFERER'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
411 $log_data['content_type'] = isset( $_SERVER['CONTENT_TYPE'] ) ? \stripslashes( $_SERVER['CONTENT_TYPE'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
412 $log_data['get_params'] = json_encode( $_GET, JSON_UNESCAPED_SLASHES );
413
414 if ( defined( 'JETPACK_WAF_SHARE_DEBUG_DATA' ) && JETPACK_WAF_SHARE_DEBUG_DATA ) {
415 $log_data['post_params'] = json_encode( $_POST, JSON_UNESCAPED_SLASHES );
416 $log_data['headers'] = self::get_request_headers();
417 }
418
419 if ( defined( 'JETPACK_WAF_SHARE_DATA' ) && JETPACK_WAF_SHARE_DATA ) {
420 $file_path = JETPACK_WAF_DIR . '/waf-blocklog';
421 $file_exists = file_exists( $file_path );
422
423 if ( ! $file_exists || filesize( $file_path ) < ( 100 * 1024 * 1024 ) ) {
424 $fp = fopen( $file_path, 'a+' );
425
426 if ( $fp ) {
427 try {
428 fwrite( $fp, json_encode( $log_data, JSON_UNESCAPED_SLASHES ) . "\n" );
429 } finally {
430 fclose( $fp );
431 }
432 }
433 }
434 }
435
436 self::write_daily_summary();
437 self::write_all_time_block_count();
438 self::write_blocklog_row( $log_data );
439 self::close_db_connection();
440 }
441 }
442