Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
67.74% covered (warning)
67.74%
84 / 124
42.86% covered (danger)
42.86%
9 / 21
CRAP
0.00% covered (danger)
0.00%
0 / 1
Waf_Runner
67.74% covered (warning)
67.74%
84 / 124
42.86% covered (danger)
42.86%
9 / 21
166.06
0.00% covered (danger)
0.00%
0 / 1
 initialize
15.38% covered (danger)
15.38%
2 / 13
0.00% covered (danger)
0.00%
0 / 1
20.15
 add_hooks
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 did_run
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 is_allowed_mode
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
2.01
 is_supported_environment
81.82% covered (warning)
81.82%
9 / 11
0.00% covered (danger)
0.00%
0 / 1
8.38
 is_provided_by_host
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 is_enabled
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 enable
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 disable
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_config
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
2
 get_bootstrap_file_path
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 get_standalone_mode_status
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 get_waf_file_path
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 run
29.41% covered (danger)
29.41%
5 / 17
0.00% covered (danger)
0.00%
0 / 1
24.23
 errorHandler
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 initialize_filesystem
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
3.07
 activate
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 initialize_waf_directory
57.14% covered (warning)
57.14%
4 / 7
0.00% covered (danger)
0.00%
0 / 1
5.26
 deactivate
75.00% covered (warning)
75.00%
6 / 8
0.00% covered (danger)
0.00%
0 / 1
3.14
 update_waf
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 automatic_rules_available
69.23% covered (warning)
69.23%
9 / 13
0.00% covered (danger)
0.00%
0 / 1
7.05
1<?php
2/**
3 * Entrypoint for actually executing the WAF.
4 *
5 * @package automattic/jetpack-waf
6 */
7
8namespace Automattic\Jetpack\Waf;
9
10use Automattic\Jetpack\Modules;
11use Automattic\Jetpack\Status\Host;
12use Automattic\Jetpack\Waf\Brute_Force_Protection\Brute_Force_Protection;
13
14/**
15 * Executes the WAF.
16 */
17class 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     * Whether the host runs its own firewall where Jetpack's can't run.
127     *
128     * @since 0.31.0
129     *
130     * @return bool
131     */
132    public static function is_provided_by_host() {
133        if ( self::is_supported_environment() ) {
134            return false;
135        }
136
137        /**
138         * Filters whether the site's host runs its own firewall in place of Jetpack's.
139         *
140         * Only changes how the firewall's status is reported, as active rather than unavailable; it blocks nothing.
141         *
142         * @since 0.31.0
143         *
144         * @param bool $provided Whether the host provides the firewall. Default false.
145         */
146        return (bool) apply_filters( 'jetpack_protect_firewall_provided_by_host', false );
147    }
148
149    /**
150     * Determines if the WAF module is enabled on the site.
151     *
152     * @return bool
153     */
154    public static function is_enabled() {
155        // if ABSPATH is defined, then WordPress has already been instantiated,
156        // so we can check to see if the waf module is activated.
157        if ( defined( 'ABSPATH' ) ) {
158            return ( new Modules() )->is_active( self::WAF_MODULE_NAME );
159        }
160
161        return true;
162    }
163
164    /**
165     * Enables the WAF module on the site.
166     *
167     * @return bool
168     */
169    public static function enable() {
170        return ( new Modules() )->activate( self::WAF_MODULE_NAME, false, false );
171    }
172
173    /**
174     * Disabled the WAF module on the site.
175     *
176     * @return bool
177     */
178    public static function disable() {
179        return ( new Modules() )->deactivate( self::WAF_MODULE_NAME );
180    }
181
182    /**
183     * Get Config
184     *
185     * @return array The WAF settings and current configuration data.
186     */
187    public static function get_config() {
188        return array(
189            Waf_Rules_Manager::AUTOMATIC_RULES_ENABLED_OPTION_NAME => Waf_Rules_Manager::automatic_rules_enabled(),
190            Waf_Rules_Manager::IP_ALLOW_LIST_OPTION_NAME => get_option( Waf_Rules_Manager::IP_ALLOW_LIST_OPTION_NAME ),
191            Waf_Rules_Manager::IP_ALLOW_LIST_ENABLED_OPTION_NAME => Waf_Rules_Manager::ip_allow_list_enabled(),
192            Waf_Rules_Manager::IP_BLOCK_LIST_OPTION_NAME => get_option( Waf_Rules_Manager::IP_BLOCK_LIST_OPTION_NAME ),
193            Waf_Rules_Manager::IP_BLOCK_LIST_ENABLED_OPTION_NAME => Waf_Rules_Manager::ip_block_list_enabled(),
194            self::SHARE_DATA_OPTION_NAME                 => get_option( self::SHARE_DATA_OPTION_NAME ),
195            self::SHARE_DEBUG_DATA_OPTION_NAME           => get_option( self::SHARE_DEBUG_DATA_OPTION_NAME ),
196            'bootstrap_path'                             => self::get_bootstrap_file_path(),
197            'standalone_mode'                            => self::get_standalone_mode_status(),
198            'automatic_rules_available'                  => (bool) self::automatic_rules_available(),
199            'brute_force_protection'                     => (bool) Brute_Force_Protection::is_enabled(),
200
201            /**
202             * Provide the deprecated IP lists options for backwards compatibility with older versions of the Jetpack and Protect plugins.
203             * i.e. If one plugin is updated and the other is not, the latest version of this package will be used by both plugins.
204             *
205             * @deprecated 0.17.0
206             */
207            // @phan-suppress-next-line PhanDeprecatedClassConstant -- Needed for backwards compatibility.
208            Waf_Rules_Manager::IP_LISTS_ENABLED_OPTION_NAME => Waf_Rules_Manager::ip_allow_list_enabled() || Waf_Rules_Manager::ip_block_list_enabled(),
209        );
210    }
211
212    /**
213     * Get Bootstrap File Path
214     *
215     * @return string The path to the Jetpack Firewall's bootstrap.php file.
216     */
217    private static function get_bootstrap_file_path() {
218        $bootstrap = new Waf_Standalone_Bootstrap();
219        return $bootstrap->get_bootstrap_file_path();
220    }
221
222    /**
223     * Get WAF standalone mode status
224     *
225     * @return bool|array True if WAF standalone mode is enabled, false otherwise.
226     */
227    public static function get_standalone_mode_status() {
228        return defined( 'JETPACK_WAF_RUN' ) && JETPACK_WAF_RUN === 'preload';
229    }
230
231    /**
232     * Get WAF File Path
233     *
234     * @param string $file The file path starting in the WAF directory.
235     * @return string The full file path to the provided file in the WAF directory.
236     */
237    public static function get_waf_file_path( $file ) {
238        Waf_Constants::define_waf_directory();
239
240        // Ensure the file path starts with a slash.
241        if ( '/' !== substr( $file, 0, 1 ) ) {
242            $file = "/$file";
243        }
244
245        return JETPACK_WAF_DIR . $file;
246    }
247
248    /**
249     * Runs the WAF and potentially stops the request if a problem is found.
250     *
251     * @return void
252     */
253    public static function run() {
254        // Make double-sure we are only running once.
255        if ( self::did_run() ) {
256            return;
257        }
258
259        Waf_Constants::initialize_constants();
260
261        // if ABSPATH is defined, then WordPress has already been instantiated,
262        // and we're running as a plugin (meh). Otherwise, we're running via something
263        // like PHP's prepend_file setting (yay!).
264        define( 'JETPACK_WAF_RUN', defined( 'ABSPATH' ) ? 'plugin' : 'preload' );
265
266        // If the WAF is being run before a command line script, or in any other non-HTTP
267        // context (e.g. server-side cron executed via a PHP wrapper that does not report
268        // PHP_SAPI as 'cli'), there is no HTTP request to evaluate. Skip rule execution so
269        // HTTP-specific rules (e.g. rule 911100, which checks the request method) don't
270        // produce a false-positive 403 block.
271        if ( PHP_SAPI === 'cli' || ! isset( $_SERVER['REQUEST_METHOD'] ) ) {
272            return;
273        }
274
275        // if something terrible happens during the WAF running, we don't want to interfere with the rest of the site,
276        // so we intercept errors ONLY while the WAF is running, then we remove our handler after the WAF finishes.
277        $display_errors = ini_get( 'display_errors' );
278
279        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.
280
281        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.
282
283        try {
284
285            // phpcs:ignore
286            $waf = new Waf_Runtime( new Waf_Transforms(), new Waf_Operators() );
287
288            // Before the rules, which stop at the IP allow list without saying why.
289            Waf_Self_Check::maybe_block( $waf );
290
291            // execute waf rules.
292            $rules_file_path = self::get_waf_file_path( JETPACK_WAF_ENTRYPOINT );
293            if ( file_exists( $rules_file_path ) ) {
294                include $rules_file_path;
295            }
296        } catch ( \Exception $err ) { // phpcs:ignore
297            // Intentionally doing nothing.
298        }
299
300        // remove the custom error handler, so we don't interfere with the site.
301        restore_error_handler();
302
303        // Restore the original value.
304        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.
305    }
306
307    /**
308     * Error handler to be used while the WAF is being executed.
309     *
310     * @param int    $code The error code.
311     * @param string $message The error message.
312     * @param string $file File with the error.
313     * @param string $line Line of the error.
314     * @return void
315     */
316    public static function errorHandler( $code, $message, $file, $line ) { // phpcs:ignore
317        // Intentionally doing nothing for now.
318    }
319
320    /**
321     * Initializes the WP filesystem and WAF directory structure.
322     *
323     * @throws File_System_Exception If filesystem is unavailable.
324     *
325     * @return void
326     */
327    public static function initialize_filesystem() {
328        if ( ! function_exists( '\\WP_Filesystem' ) ) {
329            require_once ABSPATH . 'wp-admin/includes/file.php';
330        }
331
332        if ( ! \WP_Filesystem() ) {
333            throw new File_System_Exception( 'No filesystem available.' );
334        }
335
336        self::initialize_waf_directory();
337    }
338
339    /**
340     * Activates the WAF by generating the rules script and setting the version
341     *
342     * @throws Waf_Exception If the firewall mode is invalid.
343     * @throws Waf_Exception If the activation fails.
344     *
345     * @return void
346     */
347    public static function activate() {
348        $version = get_option( Waf_Rules_Manager::VERSION_OPTION_NAME );
349        if ( ! $version ) {
350            add_option( Waf_Rules_Manager::VERSION_OPTION_NAME, Waf_Rules_Manager::RULES_VERSION );
351        }
352
353        add_option( self::SHARE_DATA_OPTION_NAME, true );
354
355        self::initialize_filesystem();
356
357        Waf_Rules_Manager::generate_automatic_rules();
358        Waf_Rules_Manager::generate_ip_rules();
359        Waf_Rules_Manager::generate_rules();
360
361        Waf_Blocklog_Manager::create_blocklog_table();
362    }
363
364    /**
365     * Ensures that the waf directory is created.
366     *
367     * @throws File_System_Exception If filesystem is unavailable.
368     * @throws File_System_Exception If creating the directory fails.
369     *
370     * @return void
371     */
372    public static function initialize_waf_directory() {
373        WP_Filesystem();
374        Waf_Constants::define_waf_directory();
375
376        global $wp_filesystem;
377        if ( ! $wp_filesystem ) {
378            throw new File_System_Exception( 'Cannot work without the file system being initialized.' );
379        }
380
381        if ( ! $wp_filesystem->is_dir( JETPACK_WAF_DIR ) ) {
382            if ( ! $wp_filesystem->mkdir( JETPACK_WAF_DIR ) ) {
383                throw new File_System_Exception( 'Failed creating WAF file directory: ' . JETPACK_WAF_DIR );
384            }
385        }
386    }
387
388    /**
389     * Deactivates the WAF by deleting the relevant options and emptying rules file.
390     *
391     * @throws File_System_Exception If file writing fails.
392     *
393     * @return void
394     */
395    public static function deactivate() {
396        delete_option( self::MODE_OPTION_NAME );
397        delete_option( Waf_Rules_Manager::VERSION_OPTION_NAME );
398
399        global $wp_filesystem;
400        self::initialize_filesystem();
401        Waf_Constants::define_entrypoint();
402
403        // If the rules file doesn't exist, there's nothing else to do.
404        if ( ! $wp_filesystem->exists( self::get_waf_file_path( JETPACK_WAF_ENTRYPOINT ) ) ) {
405            return;
406        }
407
408        // Empty the rules entrypoint file.
409        if ( ! $wp_filesystem->put_contents( self::get_waf_file_path( JETPACK_WAF_ENTRYPOINT ), "<?php\n" ) ) {
410            throw new File_System_Exception( 'Failed to empty rules.php file.' );
411        }
412    }
413
414    /**
415     * Handle updates to the WAF
416     *
417     * @return void
418     */
419    public static function update_waf() {
420        Waf_Rules_Manager::update_rules_if_changed();
421
422        // Re-generate the standalone bootstrap file on every update
423        // TODO: We may consider only doing this when the WAF version changes
424        ( new Waf_Standalone_Bootstrap() )->generate();
425    }
426
427    /**
428     * Check if an automatic rules file is available
429     *
430     * @return bool False if an automatic rules file is not available, true otherwise
431     */
432    public static function automatic_rules_available() {
433        $automatic_rules_last_updated = get_option( Waf_Rules_Manager::AUTOMATIC_RULES_LAST_UPDATED_OPTION_NAME );
434
435        // If we do not have a automatic rules last updated timestamp cached, return false.
436        if ( ! $automatic_rules_last_updated ) {
437            return false;
438        }
439
440        // Validate that the automatic rules file exists and is not empty.
441        global $wp_filesystem;
442
443        try {
444            self::initialize_filesystem();
445        } catch ( Waf_Exception $e ) {
446            return false;
447        }
448
449        $automatic_rules_file_contents = $wp_filesystem->get_contents( self::get_waf_file_path( Waf_Rules_Manager::AUTOMATIC_RULES_FILE ) );
450
451        // If the automatic rules file was removed or is now empty, return false.
452        if ( ! $automatic_rules_file_contents || "<?php\n" === $automatic_rules_file_contents ) {
453
454            // Delete the automatic rules last updated option.
455            delete_option( Waf_Rules_Manager::AUTOMATIC_RULES_LAST_UPDATED_OPTION_NAME );
456
457            // If automatic rules setting is enabled, disable it.
458            if ( Waf_Rules_Manager::automatic_rules_enabled() ) {
459                update_option( Waf_Rules_Manager::AUTOMATIC_RULES_ENABLED_OPTION_NAME, false );
460            }
461
462            return false;
463        }
464
465        return true;
466    }
467}