Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
68.33% covered (warning)
68.33%
82 / 120
40.00% covered (danger)
40.00%
8 / 20
CRAP
0.00% covered (danger)
0.00%
0 / 1
Waf_Runner
68.33% covered (warning)
68.33%
82 / 120
40.00% covered (danger)
40.00%
8 / 20
151.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_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
31.25% covered (danger)
31.25%
5 / 16
0.00% covered (danger)
0.00%
0 / 1
22.92
 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
71.43% covered (warning)
71.43%
5 / 7
0.00% covered (danger)
0.00%
0 / 1
4.37
 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     * 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}