Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WAF: Add a dedicated automatic-rules.php file for storing firewall rules #28027

Merged
merged 21 commits into from
Jan 4, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
627c697
Add a dedicated automatic-rules.php file for storing firewall rules f…
nateweller Dec 20, 2022
1e5cd85
Fix tests
nateweller Dec 20, 2022
3f11118
Update tests
nateweller Dec 21, 2022
70f04f4
Rename rule file constants
nateweller Dec 23, 2022
db9681e
Merge add/protect-waf-phase-2
nateweller Dec 23, 2022
d9ae22e
Add default option for automatic rules enabled for backwards compat
nateweller Dec 23, 2022
0da59ac
Ensure activate hooks fire on insertion of options in addition to upd…
nateweller Dec 23, 2022
92446ff
Remove unrelated changes from merge
nateweller Dec 23, 2022
4b7c1ab
Merge branch 'add/protect-waf-phase-2' into add/waf-automatic-rules-file
nateweller Dec 23, 2022
8a7fce1
Changelog
nateweller Dec 23, 2022
86dd19c
Changelog
nateweller Dec 27, 2022
32f368a
Merge branch 'add/protect-waf-phase-2' into add/waf-automatic-rules-file
nateweller Dec 27, 2022
2971dd2
Merge branch 'add/protect-waf-phase-2' into add/waf-automatic-rules-file
nateweller Dec 30, 2022
e89088b
Assorted fixes missed during merge resolution
nateweller Dec 30, 2022
ab119b2
Fix module activation when on a site level connection
nateweller Dec 30, 2022
c36b3f2
[not verified] Restore masterbar file that was accidentally formatted
nateweller Dec 30, 2022
8f3a060
Merge branch 'add/protect-waf-phase-2' into add/waf-automatic-rules-file
nateweller Jan 2, 2023
3b076b6
Refresh WAF data after connecting the site
nateweller Jan 2, 2023
20a04b6
Allow sites with old firewall rule files available to toggle automati…
nateweller Jan 4, 2023
edd135c
Fix translation minification errors
nateweller Jan 4, 2023
9e784d3
Additional translation fixes
nateweller Jan 4, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions projects/packages/waf/src/class-rest-controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ public static function update_rules() {
$message = 'Rules updated succesfully';

try {
Waf_Runner::generate_automatic_rules();
Waf_Runner::generate_rules();
} catch ( \Exception $e ) {
$success = false;
Expand Down
1 change: 1 addition & 0 deletions projects/packages/waf/src/class-waf-cli.php
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ public function teardown() {
*/
public function generate_rules() {
try {
Waf_Runner::generate_automatic_rules();
Waf_Runner::generate_rules();
} catch ( \Exception $e ) {

Expand Down
142 changes: 72 additions & 70 deletions projects/packages/waf/src/class-waf-runner.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,21 @@
*/
class Waf_Runner {

const WAF_MODULE_NAME = 'waf';
const WAF_RULES_VERSION = '1.0.0';
const MODE_OPTION_NAME = 'jetpack_waf_mode';
const AUTOMATIC_RULES_ENABLED_OPTION_NAME = 'jetpack_waf_automatic_rules';
const IP_LISTS_ENABLED_OPTION_NAME = 'jetpack_waf_ip_list';
const IP_ALLOW_LIST_OPTION_NAME = 'jetpack_waf_ip_allow_list';
const IP_BLOCK_LIST_OPTION_NAME = 'jetpack_waf_ip_block_list';
const RULES_FILE = __DIR__ . '/../rules/rules.php';
const ALLOW_IP_FILE = __DIR__ . '/../rules/allow-ip.php';
const BLOCK_IP_FILE = __DIR__ . '/../rules/block-ip.php';
const VERSION_OPTION_NAME = 'jetpack_waf_rules_version';
const RULE_LAST_UPDATED_OPTION_NAME = 'jetpack_waf_last_updated_timestamp';
const SHARE_DATA_OPTION_NAME = 'jetpack_waf_share_data';
const WAF_MODULE_NAME = 'waf';
const WAF_RULES_VERSION = '1.0.0';
const MODE_OPTION_NAME = 'jetpack_waf_mode';
const AUTOMATIC_RULES_ENABLED_OPTION_NAME = 'jetpack_waf_automatic_rules';
const IP_LISTS_ENABLED_OPTION_NAME = 'jetpack_waf_ip_list';
const IP_ALLOW_LIST_OPTION_NAME = 'jetpack_waf_ip_allow_list';
const IP_BLOCK_LIST_OPTION_NAME = 'jetpack_waf_ip_block_list';
const RULES_FILE = __DIR__ . '/../rules/rules.php';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should change the name? We have too many permutations of RULES_FILE naming and it's starting to get confused, maybe something like ENTRYPOINT_FILE?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've organized the constants a bit, and renamed them as follows via 70f04f4:

  • RULES_ENTRYPOINT_FILE
  • AUTOMATIC_RULES_FILE
  • IP_ALLOW_RULES_FILE
  • IP_BLOCK_RULES_FILE

This introduces a naming system where the sub-rule file are all named FOO_BAR_RULES_FILE while the main file is RULES_ENTRYPOINT_FILE for extra distinction.

const AUTOMATIC_RULES_FILE = __DIR__ . '/../rules/automatic-rules.php';
const ALLOW_IP_FILE = __DIR__ . '/../rules/allow-ip.php';
const BLOCK_IP_FILE = __DIR__ . '/../rules/block-ip.php';
const VERSION_OPTION_NAME = 'jetpack_waf_rules_version';
const RULE_LAST_UPDATED_OPTION_NAME = 'jetpack_waf_last_updated_timestamp';
const AUTOMATIC_RULES_LAST_UPDATED_OPTION_NAME = 'jetpack_waf_automatic_rules_last_updated_timestamp';
const SHARE_DATA_OPTION_NAME = 'jetpack_waf_share_data';

/**
* Run the WAF
Expand Down Expand Up @@ -138,24 +140,6 @@ public static function is_enabled() {
return true;
}

/**
* Determines if automatic rules are enabled.
*
* @return bool
*/
public static function automatic_rules_enabled() {
// for backwards compatibility, if the automatic rules option does not exist and the
// module is active, consider automatic rules enabled
$option_exists = get_option( self::AUTOMATIC_RULES_ENABLED_OPTION_NAME ) === false;
if ( ! $option_exists && self::is_enabled() ) {
$is_enabled = true;
} else {
$is_enabled = (bool) get_option( self::AUTOMATIC_RULES_ENABLED_OPTION_NAME );
}

return $is_enabled;
}

/**
* Enables the WAF module on the site.
*/
Expand Down Expand Up @@ -183,6 +167,7 @@ public static function get_config() {
self::IP_BLOCK_LIST_OPTION_NAME => get_option( self::IP_BLOCK_LIST_OPTION_NAME ),
self::SHARE_DATA_OPTION_NAME => get_option( self::SHARE_DATA_OPTION_NAME ),
'bootstrap_path' => self::get_bootstrap_file_path(),
'automatic_rules_available' => (bool) get_option( self::AUTOMATIC_RULES_LAST_UPDATED_OPTION_NAME ),
);
}

Expand Down Expand Up @@ -289,12 +274,11 @@ public static function activate() {
add_option( self::VERSION_OPTION_NAME, self::WAF_RULES_VERSION );
}

add_option( self::AUTOMATIC_RULES_ENABLED_OPTION_NAME, false );
add_option( self::IP_LISTS_ENABLED_OPTION_NAME, false );
add_option( self::SHARE_DATA_OPTION_NAME, true );

self::initialize_filesystem();
self::create_waf_directory();
self::generate_automatic_rules();
self::generate_ip_rules();
self::create_blocklog_table();
self::generate_rules();
Expand Down Expand Up @@ -376,6 +360,7 @@ public static function update_rules_cron() {
return;
}

self::generate_automatic_rules();
self::generate_ip_rules();
self::generate_rules();
update_option( self::RULE_LAST_UPDATED_OPTION_NAME, time() );
Expand All @@ -394,6 +379,7 @@ public static function update_rules_if_changed() {
$version = get_option( self::VERSION_OPTION_NAME );
if ( self::WAF_RULES_VERSION !== $version ) {
update_option( self::VERSION_OPTION_NAME, self::WAF_RULES_VERSION );
self::generate_automatic_rules();
self::generate_ip_rules();
self::generate_rules();
}
Expand Down Expand Up @@ -463,52 +449,75 @@ public static function generate_rules() {

self::initialize_filesystem();

$rules = "<?php\n";
$api_exception = null;
$throw_api_exception = true;

// Add automatic rules
if ( self::automatic_rules_enabled() ) {
try {
$rules = self::get_rules_from_api();
} catch ( \Exception $e ) {
if ( 401 === $e->getCode() ) {
// do not throw API exceptions for users who do not have access
$throw_api_exception = false;
}

if ( $wp_filesystem->exists( self::RULES_FILE ) && $throw_api_exception ) {
throw $e;
// Ensure all potentially required rule files exist
$rule_files = array( self::RULES_FILE, self::AUTOMATIC_RULES_FILE, self::ALLOW_IP_FILE, self::BLOCK_IP_FILE );
foreach ( $rule_files as $rule_file ) {
if ( ! $wp_filesystem->is_file( $rule_file ) ) {
if ( ! $wp_filesystem->put_contents( $rule_file, "<?php\n" ) ) {
throw new \Exception( 'Failed writing rules file to: ' . $rule_file );
}

$api_exception = $e;
}
}

// Add manual rules
$ip_allow_rules = self::ALLOW_IP_FILE;
$ip_block_rules = self::BLOCK_IP_FILE;

$ip_list_code = "if ( require('$ip_allow_rules') ) { return; }\n" .
"if ( require('$ip_block_rules') ) { return \$waf->block('block', -1, 'ip block list'); }\n";
$rules = "<?php\n";

$rules_divided_by_line = explode( "\n", $rules );
array_splice( $rules_divided_by_line, 1, 0, $ip_list_code );
// Add manual rules
if ( get_option( self::IP_LISTS_ENABLED_OPTION_NAME ) ) {
$rules .= "if ( require('" . self::ALLOW_IP_FILE . "') ) { return; }\n";
$rules .= "if ( require('" . self::BLOCK_IP_FILE . "') ) { return \$waf->block('block', -1, 'ip block list'); }\n";
}

$rules = implode( "\n", $rules_divided_by_line );
// Add automatic rules
if ( get_option( self::AUTOMATIC_RULES_ENABLED_OPTION_NAME ) ) {
$rules .= "require('" . self::AUTOMATIC_RULES_FILE . "');\n";
}

// Ensure that the folder exists.
// Ensure that the folder exists
if ( ! $wp_filesystem->is_dir( dirname( self::RULES_FILE ) ) ) {
$wp_filesystem->mkdir( dirname( self::RULES_FILE ) );
}

// Update the rules file
if ( ! $wp_filesystem->put_contents( self::RULES_FILE, $rules ) ) {
throw new \Exception( 'Failed writing rules file to: ' . self::RULES_FILE );
}
}

/**
* Generates the automatic-rules.php script
*
* @throws \Exception If rules cannot be generated and saved.
* @return void
*/
public static function generate_automatic_rules() {
/**
* WordPress filesystem abstraction.
*
* @var \WP_Filesystem_Base $wp_filesystem
*/
global $wp_filesystem;

self::initialize_filesystem();

// Ensure that the folder exists.
if ( ! $wp_filesystem->is_dir( dirname( self::AUTOMATIC_RULES_FILE ) ) ) {
$wp_filesystem->mkdir( dirname( self::AUTOMATIC_RULES_FILE ) );
}

if ( null !== $api_exception && $throw_api_exception ) {
throw $api_exception;
try {
$rules = self::get_rules_from_api();
} catch ( \Exception $exception ) {
// Do not throw API exceptions for users who do not have access
if ( 401 !== $exception->getCode() ) {
throw $exception;
}
}

if ( ! $wp_filesystem->put_contents( self::AUTOMATIC_RULES_FILE, $rules ) ) {
throw new \Exception( 'Failed writing automatic rules file to: ' . self::AUTOMATIC_RULES_FILE );
}

update_option( self::AUTOMATIC_RULES_LAST_UPDATED_OPTION_NAME, time() );
}

/**
Expand Down Expand Up @@ -558,13 +567,6 @@ public static function generate_ip_rules() {
$allow_list = self::ip_option_to_array( get_option( self::IP_ALLOW_LIST_OPTION_NAME ) );
$block_list = self::ip_option_to_array( get_option( self::IP_BLOCK_LIST_OPTION_NAME ) );

$lists_enabled = (bool) get_option( self::IP_LISTS_ENABLED_OPTION_NAME );
if ( false === $lists_enabled ) {
// Making the lists empty effectively disabled the feature while still keeping the other WAF rules evaluation active.
$allow_list = array();
$block_list = array();
}

$allow_rules_content = '';
// phpcs:disable WordPress.PHP.DevelopmentFunctions
$allow_rules_content .= '$waf_allow_list = ' . var_export( $allow_list, true ) . ";\n";
Expand Down
5 changes: 5 additions & 0 deletions projects/plugins/jetpack/tests/e2e/helpers/waf-helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,8 @@ export async function enableAutomaticRules() {
logger.sync( 'Enabling automatic firewall rules' );
return execWpCommand( 'option update jetpack_waf_automatic_rules 1' );
}

export async function generateRules() {
logger.sync( 'Generating firewall rules' );
return execWpCommand( 'jetpack-waf generate_rules' );
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { WpPage } from 'jetpack-e2e-commons/pages/index.js';
import playwrightConfig from '../../playwright.config.cjs';
import { Plans, prerequisitesBuilder } from 'jetpack-e2e-commons/env/index.js';
import { resolveSiteUrl } from 'jetpack-e2e-commons/helpers/utils-helper.cjs';
import { enableAutomaticRules } from '../../helpers/waf-helper.js';
import { enableAutomaticRules, generateRules } from '../../helpers/waf-helper.js';

test.describe.parallel( 'WAF Blocking', () => {
test.beforeAll( async ( { browser } ) => {
Expand All @@ -20,6 +20,7 @@ test.describe.parallel( 'WAF Blocking', () => {
.withActiveModules( [ 'waf' ] )
.build();
await enableAutomaticRules();
await generateRules();
await page.close();
} );

Expand Down