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-runner.php

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

441 lines 13.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Entrypoint for actually executing the WAF.
4 *
5 * @package automattic/jetpack-waf
6 */
7
8 namespace Automattic\Jetpack\Waf;
9
10 use Automattic\Jetpack\Modules;
11 use Automattic\Jetpack\Status\Host;
12 use Automattic\Jetpack\Waf\Brute_Force_Protection\Brute_Force_Protection;
13
14 /**
15 * Executes the WAF.
16 */
17 class Waf_Runner {
18
19 const WAF_MODULE_NAME = 'waf';
20 const MODE_OPTION_NAME = 'jetpack_waf_mode';
21 const SHARE_DATA_OPTION_NAME = 'jetpack_waf_share_data';
22 const SHARE_DEBUG_DATA_OPTION_NAME = 'jetpack_waf_share_debug_data';
23
24 /**
25 * Run the WAF
26 *
27 * @return void
28 */
29 public static function initialize() {
30 if ( ! self::is_enabled() ) {
31 return;
32 }
33 Waf_Constants::define_mode();
34 Waf_Constants::define_entrypoint();
35 Waf_Constants::define_share_data();
36
37 if ( ! self::is_allowed_mode( JETPACK_WAF_MODE ) ) {
38 return;
39 }
40 // Don't run if in standalone mode
41 if ( function_exists( 'add_action' ) ) {
42 self::add_hooks();
43 Waf_Rules_Manager::add_hooks();
44 Waf_Rules_Manager::schedule_rules_cron();
45 }
46 if ( ! self::did_run() ) {
47 self::run();
48 }
49 }
50
51 /**
52 * Set action hooks
53 *
54 * @return void
55 */
56 public static function add_hooks() {
57 // Register REST routes. Use a static callable so the controller class is not
58 // loaded into memory/opcache on requests that never reach `rest_api_init`.
59 add_action( 'rest_api_init', array( REST_Controller::class, 'register_rest_routes' ) );
60 }
61
62 /**
63 * Did the WAF run yet or not?
64 *
65 * @return bool
66 */
67 public static function did_run() {
68 return defined( 'JETPACK_WAF_RUN' );
69 }
70
71 /**
72 * Determines if the passed $option is one of the allowed WAF operation modes.
73 *
74 * @param string $option The mode option.
75 * @return bool
76 */
77 public static function is_allowed_mode( $option ) {
78 // Normal constants are defined prior to WP_CLI running causing problems for activation
79 if ( defined( 'WAF_CLI_MODE' ) ) {
80 $option = WAF_CLI_MODE;
81 }
82
83 $allowed_modes = array(
84 'normal',
85 'silent',
86 );
87
88 return in_array( $option, $allowed_modes, true );
89 }
90
91 /**
92 * Determines if the WAF is supported in the current environment.
93 *
94 * @since 0.8.0
95 * @return bool
96 */
97 public static function is_supported_environment() {
98 // Do not run when killswitch is enabled
99 if ( defined( 'DISABLE_JETPACK_WAF' ) && DISABLE_JETPACK_WAF ) {
100 return false;
101 }
102
103 if ( defined( 'IS_ATOMIC_JN' ) && IS_ATOMIC_JN ) {
104 return true;
105 }
106
107 // Do not run in the WPCOM context
108 if ( ( new Host() )->is_wpcom_simple() ) {
109 return false;
110 }
111
112 // Do not run on the Atomic platform
113 if ( ( new Host() )->is_atomic_platform() ) {
114 return false;
115 }
116
117 // Do not run on the VIP platform
118 if ( ( new Host() )->is_vip_site() ) {
119 return false;
120 }
121
122 return true;
123 }
124
125 /**
126 * Determines if the WAF module is enabled on the site.
127 *
128 * @return bool
129 */
130 public static function is_enabled() {
131 // if ABSPATH is defined, then WordPress has already been instantiated,
132 // so we can check to see if the waf module is activated.
133 if ( defined( 'ABSPATH' ) ) {
134 return ( new Modules() )->is_active( self::WAF_MODULE_NAME );
135 }
136
137 return true;
138 }
139
140 /**
141 * Enables the WAF module on the site.
142 *
143 * @return bool
144 */
145 public static function enable() {
146 return ( new Modules() )->activate( self::WAF_MODULE_NAME, false, false );
147 }
148
149 /**
150 * Disabled the WAF module on the site.
151 *
152 * @return bool
153 */
154 public static function disable() {
155 return ( new Modules() )->deactivate( self::WAF_MODULE_NAME );
156 }
157
158 /**
159 * Get Config
160 *
161 * @return array The WAF settings and current configuration data.
162 */
163 public static function get_config() {
164 return array(
165 Waf_Rules_Manager::AUTOMATIC_RULES_ENABLED_OPTION_NAME => Waf_Rules_Manager::automatic_rules_enabled(),
166 Waf_Rules_Manager::IP_ALLOW_LIST_OPTION_NAME => get_option( Waf_Rules_Manager::IP_ALLOW_LIST_OPTION_NAME ),
167 Waf_Rules_Manager::IP_ALLOW_LIST_ENABLED_OPTION_NAME => Waf_Rules_Manager::ip_allow_list_enabled(),
168 Waf_Rules_Manager::IP_BLOCK_LIST_OPTION_NAME => get_option( Waf_Rules_Manager::IP_BLOCK_LIST_OPTION_NAME ),
169 Waf_Rules_Manager::IP_BLOCK_LIST_ENABLED_OPTION_NAME => Waf_Rules_Manager::ip_block_list_enabled(),
170 self::SHARE_DATA_OPTION_NAME => get_option( self::SHARE_DATA_OPTION_NAME ),
171 self::SHARE_DEBUG_DATA_OPTION_NAME => get_option( self::SHARE_DEBUG_DATA_OPTION_NAME ),
172 'bootstrap_path' => self::get_bootstrap_file_path(),
173 'standalone_mode' => self::get_standalone_mode_status(),
174 'automatic_rules_available' => (bool) self::automatic_rules_available(),
175 'brute_force_protection' => (bool) Brute_Force_Protection::is_enabled(),
176
177 /**
178 * Provide the deprecated IP lists options for backwards compatibility with older versions of the Jetpack and Protect plugins.
179 * i.e. If one plugin is updated and the other is not, the latest version of this package will be used by both plugins.
180 *
181 * @deprecated 0.17.0
182 */
183 // @phan-suppress-next-line PhanDeprecatedClassConstant -- Needed for backwards compatibility.
184 Waf_Rules_Manager::IP_LISTS_ENABLED_OPTION_NAME => Waf_Rules_Manager::ip_allow_list_enabled() || Waf_Rules_Manager::ip_block_list_enabled(),
185 );
186 }
187
188 /**
189 * Get Bootstrap File Path
190 *
191 * @return string The path to the Jetpack Firewall's bootstrap.php file.
192 */
193 private static function get_bootstrap_file_path() {
194 $bootstrap = new Waf_Standalone_Bootstrap();
195 return $bootstrap->get_bootstrap_file_path();
196 }
197
198 /**
199 * Get WAF standalone mode status
200 *
201 * @return bool|array True if WAF standalone mode is enabled, false otherwise.
202 */
203 public static function get_standalone_mode_status() {
204 return defined( 'JETPACK_WAF_RUN' ) && JETPACK_WAF_RUN === 'preload';
205 }
206
207 /**
208 * Get WAF File Path
209 *
210 * @param string $file The file path starting in the WAF directory.
211 * @return string The full file path to the provided file in the WAF directory.
212 */
213 public static function get_waf_file_path( $file ) {
214 Waf_Constants::define_waf_directory();
215
216 // Ensure the file path starts with a slash.
217 if ( '/' !== substr( $file, 0, 1 ) ) {
218 $file = "/$file";
219 }
220
221 return JETPACK_WAF_DIR . $file;
222 }
223
224 /**
225 * Runs the WAF and potentially stops the request if a problem is found.
226 *
227 * @return void
228 */
229 public static function run() {
230 // Make double-sure we are only running once.
231 if ( self::did_run() ) {
232 return;
233 }
234
235 Waf_Constants::initialize_constants();
236
237 // if ABSPATH is defined, then WordPress has already been instantiated,
238 // and we're running as a plugin (meh). Otherwise, we're running via something
239 // like PHP's prepend_file setting (yay!).
240 define( 'JETPACK_WAF_RUN', defined( 'ABSPATH' ) ? 'plugin' : 'preload' );
241
242 // If the WAF is being run before a command line script, or in any other non-HTTP
243 // context (e.g. server-side cron executed via a PHP wrapper that does not report
244 // PHP_SAPI as 'cli'), there is no HTTP request to evaluate. Skip rule execution so
245 // HTTP-specific rules (e.g. rule 911100, which checks the request method) don't
246 // produce a false-positive 403 block.
247 if ( PHP_SAPI === 'cli' || ! isset( $_SERVER['REQUEST_METHOD'] ) ) {
248 return;
249 }
250
251 // if something terrible happens during the WAF running, we don't want to interfere with the rest of the site,
252 // so we intercept errors ONLY while the WAF is running, then we remove our handler after the WAF finishes.
253 $display_errors = ini_get( 'display_errors' );
254
255 ini_set( 'display_errors', 'Off' ); // phpcs:ignore WordPress.PHP.IniSet.display_errors_Disallowed -- We only customize error reporting while the WAF is running, and remove our handler afterwards.
256
257 set_error_handler( array( self::class, 'errorHandler' ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_set_error_handler -- We only customize error reporting while the WAF is running, and remove our handler afterwards.
258
259 try {
260
261 // phpcs:ignore
262 $waf = new Waf_Runtime( new Waf_Transforms(), new Waf_Operators() );
263
264 // execute waf rules.
265 $rules_file_path = self::get_waf_file_path( JETPACK_WAF_ENTRYPOINT );
266 if ( file_exists( $rules_file_path ) ) {
267 include $rules_file_path;
268 }
269 } catch ( \Exception $err ) { // phpcs:ignore
270 // Intentionally doing nothing.
271 }
272
273 // remove the custom error handler, so we don't interfere with the site.
274 restore_error_handler();
275
276 // Restore the original value.
277 ini_set( 'display_errors', $display_errors ); // phpcs:ignore WordPress.PHP.IniSet.display_errors_Disallowed -- We only customize error reporting while the WAF is running, and remove our handler afterwards.
278 }
279
280 /**
281 * Error handler to be used while the WAF is being executed.
282 *
283 * @param int $code The error code.
284 * @param string $message The error message.
285 * @param string $file File with the error.
286 * @param string $line Line of the error.
287 * @return void
288 */
289 public static function errorHandler( $code, $message, $file, $line ) { // phpcs:ignore
290 // Intentionally doing nothing for now.
291 }
292
293 /**
294 * Initializes the WP filesystem and WAF directory structure.
295 *
296 * @throws File_System_Exception If filesystem is unavailable.
297 *
298 * @return void
299 */
300 public static function initialize_filesystem() {
301 if ( ! function_exists( '\\WP_Filesystem' ) ) {
302 require_once ABSPATH . 'wp-admin/includes/file.php';
303 }
304
305 if ( ! \WP_Filesystem() ) {
306 throw new File_System_Exception( 'No filesystem available.' );
307 }
308
309 self::initialize_waf_directory();
310 }
311
312 /**
313 * Activates the WAF by generating the rules script and setting the version
314 *
315 * @throws Waf_Exception If the firewall mode is invalid.
316 * @throws Waf_Exception If the activation fails.
317 *
318 * @return void
319 */
320 public static function activate() {
321 $version = get_option( Waf_Rules_Manager::VERSION_OPTION_NAME );
322 if ( ! $version ) {
323 add_option( Waf_Rules_Manager::VERSION_OPTION_NAME, Waf_Rules_Manager::RULES_VERSION );
324 }
325
326 add_option( self::SHARE_DATA_OPTION_NAME, true );
327
328 self::initialize_filesystem();
329
330 Waf_Rules_Manager::generate_automatic_rules();
331 Waf_Rules_Manager::generate_ip_rules();
332 Waf_Rules_Manager::generate_rules();
333
334 Waf_Blocklog_Manager::create_blocklog_table();
335 }
336
337 /**
338 * Ensures that the waf directory is created.
339 *
340 * @throws File_System_Exception If filesystem is unavailable.
341 * @throws File_System_Exception If creating the directory fails.
342 *
343 * @return void
344 */
345 public static function initialize_waf_directory() {
346 WP_Filesystem();
347 Waf_Constants::define_waf_directory();
348
349 global $wp_filesystem;
350 if ( ! $wp_filesystem ) {
351 throw new File_System_Exception( 'Cannot work without the file system being initialized.' );
352 }
353
354 if ( ! $wp_filesystem->is_dir( JETPACK_WAF_DIR ) ) {
355 if ( ! $wp_filesystem->mkdir( JETPACK_WAF_DIR ) ) {
356 throw new File_System_Exception( 'Failed creating WAF file directory: ' . JETPACK_WAF_DIR );
357 }
358 }
359 }
360
361 /**
362 * Deactivates the WAF by deleting the relevant options and emptying rules file.
363 *
364 * @throws File_System_Exception If file writing fails.
365 *
366 * @return void
367 */
368 public static function deactivate() {
369 delete_option( self::MODE_OPTION_NAME );
370 delete_option( Waf_Rules_Manager::VERSION_OPTION_NAME );
371
372 global $wp_filesystem;
373 self::initialize_filesystem();
374 Waf_Constants::define_entrypoint();
375
376 // If the rules file doesn't exist, there's nothing else to do.
377 if ( ! $wp_filesystem->exists( self::get_waf_file_path( JETPACK_WAF_ENTRYPOINT ) ) ) {
378 return;
379 }
380
381 // Empty the rules entrypoint file.
382 if ( ! $wp_filesystem->put_contents( self::get_waf_file_path( JETPACK_WAF_ENTRYPOINT ), "<?php\n" ) ) {
383 throw new File_System_Exception( 'Failed to empty rules.php file.' );
384 }
385 }
386
387 /**
388 * Handle updates to the WAF
389 *
390 * @return void
391 */
392 public static function update_waf() {
393 Waf_Rules_Manager::update_rules_if_changed();
394
395 // Re-generate the standalone bootstrap file on every update
396 // TODO: We may consider only doing this when the WAF version changes
397 ( new Waf_Standalone_Bootstrap() )->generate();
398 }
399
400 /**
401 * Check if an automatic rules file is available
402 *
403 * @return bool False if an automatic rules file is not available, true otherwise
404 */
405 public static function automatic_rules_available() {
406 $automatic_rules_last_updated = get_option( Waf_Rules_Manager::AUTOMATIC_RULES_LAST_UPDATED_OPTION_NAME );
407
408 // If we do not have a automatic rules last updated timestamp cached, return false.
409 if ( ! $automatic_rules_last_updated ) {
410 return false;
411 }
412
413 // Validate that the automatic rules file exists and is not empty.
414 global $wp_filesystem;
415
416 try {
417 self::initialize_filesystem();
418 } catch ( Waf_Exception $e ) {
419 return false;
420 }
421
422 $automatic_rules_file_contents = $wp_filesystem->get_contents( self::get_waf_file_path( Waf_Rules_Manager::AUTOMATIC_RULES_FILE ) );
423
424 // If the automatic rules file was removed or is now empty, return false.
425 if ( ! $automatic_rules_file_contents || "<?php\n" === $automatic_rules_file_contents ) {
426
427 // Delete the automatic rules last updated option.
428 delete_option( Waf_Rules_Manager::AUTOMATIC_RULES_LAST_UPDATED_OPTION_NAME );
429
430 // If automatic rules setting is enabled, disable it.
431 if ( Waf_Rules_Manager::automatic_rules_enabled() ) {
432 update_option( Waf_Rules_Manager::AUTOMATIC_RULES_ENABLED_OPTION_NAME, false );
433 }
434
435 return false;
436 }
437
438 return true;
439 }
440 }
441