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