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-rules-manager.php

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

363 lines 12.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Class for generating and working with firewall rule files.
4 *
5 * @since 0.9.0
6 *
7 * @package automattic/jetpack-waf
8 */
9
10 namespace Automattic\Jetpack\Waf;
11
12 use Automattic\Jetpack\Connection\Client;
13 use Automattic\Jetpack\IP\Utils as IP_Utils;
14 use Jetpack_Options;
15 use WP_Error;
16
17 /**
18 * Class for generating and working with firewall rule files.
19 */
20 class Waf_Rules_Manager {
21
22 const RULES_VERSION = '1.0.0';
23
24 // WAF Options
25 const VERSION_OPTION_NAME = 'jetpack_waf_rules_version';
26 const AUTOMATIC_RULES_ENABLED_OPTION_NAME = 'jetpack_waf_automatic_rules';
27 const IP_ALLOW_LIST_OPTION_NAME = 'jetpack_waf_ip_allow_list';
28 const IP_ALLOW_LIST_ENABLED_OPTION_NAME = 'jetpack_waf_ip_allow_list_enabled';
29 const IP_BLOCK_LIST_OPTION_NAME = 'jetpack_waf_ip_block_list';
30 const IP_BLOCK_LIST_ENABLED_OPTION_NAME = 'jetpack_waf_ip_block_list_enabled';
31 const RULE_LAST_UPDATED_OPTION_NAME = 'jetpack_waf_last_updated_timestamp';
32 const AUTOMATIC_RULES_LAST_UPDATED_OPTION_NAME = 'jetpack_waf_automatic_rules_last_updated_timestamp';
33
34 /**
35 * IP Lists Enabled Option Name
36 *
37 * @deprecated 0.17.0 Use Waf_Rules_Manager::IP_ALLOW_LIST_ENABLED_OPTION_NAME and Waf_Rules_Manager::IP_BLOCK_LIST_ENABLED_OPTION_NAME instead.
38 */
39 const IP_LISTS_ENABLED_OPTION_NAME = 'jetpack_waf_ip_list';
40
41 // Rule Files
42 const AUTOMATIC_RULES_FILE = '/rules/automatic-rules.php';
43 const IP_ALLOW_RULES_FILE = '/rules/allow-ip.php';
44 const IP_BLOCK_RULES_FILE = '/rules/block-ip.php';
45
46 /**
47 * Rules Entrypoint File
48 *
49 * @deprecated 0.22.0 Use JETPACK_WAF_ENTRYPOINT instead.
50 */
51 const RULES_ENTRYPOINT_FILE = '/rules/rules.php';
52
53 /**
54 * Whether automatic rules are enabled.
55 *
56 * @return bool
57 */
58 public static function automatic_rules_enabled() {
59 return (bool) get_option( self::AUTOMATIC_RULES_ENABLED_OPTION_NAME );
60 }
61
62 /**
63 * Whether IP allow list is enabled.
64 *
65 * @return bool
66 */
67 public static function ip_allow_list_enabled() {
68 return (bool) get_option( self::IP_ALLOW_LIST_ENABLED_OPTION_NAME );
69 }
70
71 /**
72 * Whether IP block list is enabled.
73 *
74 * @return bool
75 */
76 public static function ip_block_list_enabled() {
77 return (bool) get_option( self::IP_BLOCK_LIST_ENABLED_OPTION_NAME );
78 }
79
80 /**
81 * Register WordPress hooks for the WAF rules.
82 *
83 * @return void
84 */
85 public static function add_hooks() {
86 // Re-activate the WAF any time an option is added or updated.
87 add_action( 'add_option_' . self::AUTOMATIC_RULES_ENABLED_OPTION_NAME, array( static::class, 'reactivate_on_rules_option_change' ), 10, 0 );
88 add_action( 'update_option_' . self::AUTOMATIC_RULES_ENABLED_OPTION_NAME, array( static::class, 'reactivate_on_rules_option_change' ), 10, 0 );
89 add_action( 'add_option_' . self::IP_ALLOW_LIST_ENABLED_OPTION_NAME, array( static::class, 'reactivate_on_rules_option_change' ), 10, 0 );
90 add_action( 'update_option_' . self::IP_ALLOW_LIST_ENABLED_OPTION_NAME, array( static::class, 'reactivate_on_rules_option_change' ), 10, 0 );
91 add_action( 'add_option_' . self::IP_ALLOW_LIST_OPTION_NAME, array( static::class, 'reactivate_on_rules_option_change' ), 10, 0 );
92 add_action( 'update_option_' . self::IP_ALLOW_LIST_OPTION_NAME, array( static::class, 'reactivate_on_rules_option_change' ), 10, 0 );
93 add_action( 'add_option_' . self::IP_BLOCK_LIST_ENABLED_OPTION_NAME, array( static::class, 'reactivate_on_rules_option_change' ), 10, 0 );
94 add_action( 'update_option_' . self::IP_BLOCK_LIST_ENABLED_OPTION_NAME, array( static::class, 'reactivate_on_rules_option_change' ), 10, 0 );
95 add_action( 'add_option_' . self::IP_BLOCK_LIST_OPTION_NAME, array( static::class, 'reactivate_on_rules_option_change' ), 10, 0 );
96 add_action( 'update_option_' . self::IP_BLOCK_LIST_OPTION_NAME, array( static::class, 'reactivate_on_rules_option_change' ), 10, 0 );
97 // Register the cron job.
98 add_action( 'jetpack_waf_rules_update_cron', array( static::class, 'update_rules_cron' ) );
99 }
100
101 /**
102 * Schedule the cron job to update the WAF rules.
103 *
104 * @return bool|WP_Error True if the event is scheduled, WP_Error on failure.
105 */
106 public static function schedule_rules_cron() {
107 if ( ! wp_next_scheduled( 'jetpack_waf_rules_update_cron' ) ) {
108 return wp_schedule_event( time(), 'twicedaily', 'jetpack_waf_rules_update_cron', array(), true );
109 }
110
111 return true;
112 }
113
114 /**
115 * Tries periodically to update the rules using our API.
116 *
117 * @return bool|WP_Error True if rules update is successful, WP_Error on failure.
118 */
119 public static function update_rules_cron() {
120 try {
121 self::generate_automatic_rules();
122 self::generate_ip_rules();
123 self::generate_rules();
124 } catch ( Waf_Exception $e ) {
125 return $e->get_wp_error();
126 }
127
128 update_option( self::RULE_LAST_UPDATED_OPTION_NAME, time() );
129 return true;
130 }
131
132 /**
133 * Re-activate the WAF any time an option is added or updated.
134 *
135 * @return bool|WP_Error True if re-activation is successful, WP_Error on failure.
136 */
137 public static function reactivate_on_rules_option_change() {
138 try {
139 Waf_Runner::activate();
140 } catch ( Waf_Exception $e ) {
141 return $e->get_wp_error();
142 }
143
144 return true;
145 }
146
147 /**
148 * Updates the rule set if rules version has changed
149 *
150 * @throws Waf_Exception If the firewall mode is invalid.
151 * @throws Waf_Exception If the rules update fails.
152 *
153 * @return void
154 */
155 public static function update_rules_if_changed() {
156 $version = get_option( self::VERSION_OPTION_NAME );
157 if ( self::RULES_VERSION !== $version ) {
158 self::generate_automatic_rules();
159 self::generate_ip_rules();
160 self::generate_rules();
161
162 update_option( self::VERSION_OPTION_NAME, self::RULES_VERSION );
163 }
164 }
165
166 /**
167 * Retrieve rules from the API
168 *
169 * @throws Waf_Exception If site is not registered.
170 * @throws Rules_API_Exception If API did not respond 200.
171 * @throws Rules_API_Exception If data is missing from response.
172 *
173 * @return array
174 */
175 public static function get_rules_from_api() {
176 $blog_id = Jetpack_Options::get_option( 'id' );
177 if ( ! $blog_id ) {
178 throw new Waf_Exception( 'Site is not registered' );
179 }
180
181 $response = Client::wpcom_json_api_request_as_blog(
182 sprintf( '/sites/%s/waf-rules', $blog_id ),
183 '2',
184 array(),
185 null,
186 'wpcom'
187 );
188
189 $response_code = wp_remote_retrieve_response_code( $response );
190
191 if ( 200 !== $response_code ) {
192 throw new Rules_API_Exception( 'API connection failed.', (int) $response_code );
193 }
194
195 $rules_json = wp_remote_retrieve_body( $response );
196 $rules = json_decode( $rules_json, true );
197
198 if ( empty( $rules['data'] ) ) {
199 throw new Rules_API_Exception( 'Data missing from response.' );
200 }
201
202 return $rules['data'];
203 }
204
205 /**
206 * Wraps a require statement in a file_exists check.
207 *
208 * @param string $required_file The file to check if exists and require.
209 * @param string $return_code The PHP code to execute if the file require returns true. Defaults to 'return;'.
210 *
211 * @return string The wrapped require statement.
212 */
213 private static function wrap_require( $required_file, $return_code = 'return;' ) {
214 return "if ( file_exists( '$required_file' ) ) { if ( require( '$required_file' ) ) { $return_code } }";
215 }
216
217 /**
218 * Generates the rules.php script
219 *
220 * @global \WP_Filesystem_Base $wp_filesystem WordPress filesystem abstraction.
221 *
222 * @throws File_System_Exception If file writing fails initializing rule files.
223 * @throws File_System_Exception If file writing fails writing to the rules entrypoint file.
224 *
225 * @return void
226 */
227 public static function generate_rules() {
228 global $wp_filesystem;
229 Waf_Runner::initialize_filesystem();
230 Waf_Constants::define_entrypoint();
231
232 $rules = "<?php\n";
233 $entrypoint_file_path = Waf_Runner::get_waf_file_path( JETPACK_WAF_ENTRYPOINT );
234
235 // Ensure that the folder exists
236 if ( ! $wp_filesystem->is_dir( dirname( $entrypoint_file_path ) ) ) {
237 $wp_filesystem->mkdir( dirname( $entrypoint_file_path ) );
238 }
239
240 // Ensure all potentially required rule files exist
241 $rule_files = array( JETPACK_WAF_ENTRYPOINT, self::AUTOMATIC_RULES_FILE, self::IP_ALLOW_RULES_FILE, self::IP_BLOCK_RULES_FILE );
242 foreach ( $rule_files as $rule_file ) {
243 $rule_file = Waf_Runner::get_waf_file_path( $rule_file );
244 if ( ! $wp_filesystem->is_file( $rule_file ) ) {
245 if ( ! $wp_filesystem->put_contents( $rule_file, "<?php\n" ) ) {
246 throw new File_System_Exception( 'Failed writing rules file to: ' . $rule_file );
247 }
248 }
249 }
250
251 // Add IP allow list
252 if ( self::ip_allow_list_enabled() ) {
253 $rules .= self::wrap_require( Waf_Runner::get_waf_file_path( self::IP_ALLOW_RULES_FILE ) ) . "\n";
254 }
255
256 // Add IP block list
257 if ( self::ip_block_list_enabled() ) {
258 $rules .= self::wrap_require( Waf_Runner::get_waf_file_path( self::IP_BLOCK_RULES_FILE ), "return \$waf->block( 'block', -1, 'ip block list' );" ) . "\n";
259 }
260
261 // Add automatic rules
262 if ( self::automatic_rules_enabled() ) {
263 $rules .= self::wrap_require( Waf_Runner::get_waf_file_path( self::AUTOMATIC_RULES_FILE ) ) . "\n";
264 }
265
266 // Update the rules file
267 if ( ! $wp_filesystem->put_contents( $entrypoint_file_path, $rules ) ) {
268 throw new File_System_Exception( 'Failed writing rules file to: ' . $entrypoint_file_path );
269 }
270 }
271
272 /**
273 * Generates the automatic-rules.php script
274 *
275 * @global \WP_Filesystem_Base $wp_filesystem WordPress filesystem abstraction.
276 *
277 * @throws Waf_Exception If rules cannot be fetched from the API.
278 * @throws File_System_Exception If file writing fails.
279 *
280 * @return void
281 */
282 public static function generate_automatic_rules() {
283 global $wp_filesystem;
284 Waf_Runner::initialize_filesystem();
285
286 $automatic_rules_file_path = Waf_Runner::get_waf_file_path( self::AUTOMATIC_RULES_FILE );
287
288 // Ensure that the folder exists.
289 if ( ! $wp_filesystem->is_dir( dirname( $automatic_rules_file_path ) ) ) {
290 $wp_filesystem->mkdir( dirname( $automatic_rules_file_path ) );
291 }
292
293 try {
294 $rules = self::get_rules_from_api();
295 } catch ( Waf_Exception $e ) {
296 // Do not throw API exceptions for users who do not have access
297 if ( 401 !== $e->getCode() ) {
298 throw $e;
299 }
300 }
301
302 // If there are no rules available, don't overwrite the existing file.
303 if ( empty( $rules ) ) {
304 return;
305 }
306
307 if ( ! $wp_filesystem->put_contents( $automatic_rules_file_path, $rules ) ) {
308 throw new File_System_Exception( 'Failed writing automatic rules file to: ' . $automatic_rules_file_path );
309 }
310
311 update_option( self::AUTOMATIC_RULES_LAST_UPDATED_OPTION_NAME, time() );
312 }
313
314 /**
315 * Generates the rules.php script
316 *
317 * @global \WP_Filesystem_Base $wp_filesystem WordPress filesystem abstraction.
318 *
319 * @throws File_System_Exception If writing to IP allow list file fails.
320 * @throws File_System_Exception If writing to IP block list file fails.
321 *
322 * @return void
323 */
324 public static function generate_ip_rules() {
325 global $wp_filesystem;
326 Waf_Runner::initialize_filesystem();
327
328 $allow_ip_file_path = Waf_Runner::get_waf_file_path( self::IP_ALLOW_RULES_FILE );
329 $block_ip_file_path = Waf_Runner::get_waf_file_path( self::IP_BLOCK_RULES_FILE );
330
331 // Ensure that the folders exists.
332 if ( ! $wp_filesystem->is_dir( dirname( $allow_ip_file_path ) ) ) {
333 $wp_filesystem->mkdir( dirname( $allow_ip_file_path ) );
334 }
335 if ( ! $wp_filesystem->is_dir( dirname( $block_ip_file_path ) ) ) {
336 $wp_filesystem->mkdir( dirname( $block_ip_file_path ) );
337 }
338
339 $allow_list = IP_Utils::get_ip_addresses_from_string( get_option( self::IP_ALLOW_LIST_OPTION_NAME ) );
340 $block_list = IP_Utils::get_ip_addresses_from_string( get_option( self::IP_BLOCK_LIST_OPTION_NAME ) );
341
342 $allow_rules_content = '';
343 // phpcs:disable WordPress.PHP.DevelopmentFunctions
344 $allow_rules_content .= '$waf_allow_list = ' . var_export( $allow_list, true ) . ";\n";
345 // phpcs:enable
346 $allow_rules_content .= 'return $waf->is_ip_in_array( $waf_allow_list );' . "\n";
347
348 if ( ! $wp_filesystem->put_contents( $allow_ip_file_path, "<?php\n$allow_rules_content" ) ) {
349 throw new File_System_Exception( 'Failed writing allow list file to: ' . $allow_ip_file_path );
350 }
351
352 $block_rules_content = '';
353 // phpcs:disable WordPress.PHP.DevelopmentFunctions
354 $block_rules_content .= '$waf_block_list = ' . var_export( $block_list, true ) . ";\n";
355 // phpcs:enable
356 $block_rules_content .= 'return $waf->is_ip_in_array( $waf_block_list );' . "\n";
357
358 if ( ! $wp_filesystem->put_contents( $block_ip_file_path, "<?php\n$block_rules_content" ) ) {
359 throw new File_System_Exception( 'Failed writing block list file to: ' . $block_ip_file_path );
360 }
361 }
362 }
363