updated plugin Jetpack Protect version 2.1.0

This commit is contained in:
2024-04-19 10:49:36 +00:00
committed by Gitium
parent 620280b550
commit 7841fd5dc6
179 changed files with 6360 additions and 1476 deletions

View File

@ -5,6 +5,38 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.16.0.1] - 2024-04-10
### Security
- Improves handling of REQUEST_URI [#36833]
## [0.16.0] - 2024-03-22
### Added
- Add data to WAF logs and add toggle for users to opt-in to share more data with us if needed. [#36377]
## [0.15.1] - 2024-03-14
### Changed
- Internal updates.
## [0.15.0] - 2024-03-12
### Added
- Add JSON parameter support to the Web Application Firewall. [#36169]
## [0.14.2] - 2024-03-04
### Fixed
- Fixed base64 transforms to better conform with the modsecurity runtime [#35693]
## [0.14.1] - 2024-02-27
### Changed
- Internal updates.
## [0.14.0] - 2024-02-12
### Added
- Add standalone mode status to WAF config [#34840]
## [0.13.0] - 2024-02-05
### Added
- Run the WAF on JN environments [#35341]
## [0.12.4] - 2024-01-18
### Fixed
- Optimize how the web application firewall checks for updates on admin screens. [#34820]
@ -257,6 +289,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Core: do not ship .phpcs.dir.xml in production builds.
[0.16.0.1]: https://github.com/Automattic/jetpack-waf/compare/v0.16.0...v0.16.0.1
[0.16.0]: https://github.com/Automattic/jetpack-waf/compare/v0.15.1...v0.16.0
[0.15.1]: https://github.com/Automattic/jetpack-waf/compare/v0.15.0...v0.15.1
[0.15.0]: https://github.com/Automattic/jetpack-waf/compare/v0.14.2...v0.15.0
[0.14.2]: https://github.com/Automattic/jetpack-waf/compare/v0.14.1...v0.14.2
[0.14.1]: https://github.com/Automattic/jetpack-waf/compare/v0.14.0...v0.14.1
[0.14.0]: https://github.com/Automattic/jetpack-waf/compare/v0.13.0...v0.14.0
[0.13.0]: https://github.com/Automattic/jetpack-waf/compare/v0.12.4...v0.13.0
[0.12.4]: https://github.com/Automattic/jetpack-waf/compare/v0.12.3...v0.12.4
[0.12.3]: https://github.com/Automattic/jetpack-waf/compare/v0.12.2...v0.12.3
[0.12.2]: https://github.com/Automattic/jetpack-waf/compare/v0.12.1...v0.12.2

View File

@ -5,15 +5,15 @@
"license": "GPL-2.0-or-later",
"require": {
"php": ">=7.0",
"automattic/jetpack-connection": "^2.2.0",
"automattic/jetpack-constants": "^2.0.0",
"automattic/jetpack-ip": "^0.2.1",
"automattic/jetpack-status": "^2.1.0",
"automattic/jetpack-connection": "^2.4.1",
"automattic/jetpack-constants": "^2.0.1",
"automattic/jetpack-ip": "^0.2.2",
"automattic/jetpack-status": "^2.1.2",
"wikimedia/aho-corasick": "^1.0"
},
"require-dev": {
"yoast/phpunit-polyfills": "1.1.0",
"automattic/jetpack-changelogger": "^4.0.5",
"automattic/jetpack-changelogger": "^4.1.1",
"automattic/wordbless": "@dev"
},
"suggest": {
@ -52,7 +52,7 @@
"link-template": "https://github.com/Automattic/jetpack-waf/compare/v${old}...v${new}"
},
"branch-alias": {
"dev-trunk": "0.12.x-dev"
"dev-trunk": "0.16.x-dev"
}
},
"config": {

View File

@ -121,9 +121,24 @@ class REST_Controller {
// Share Data
if ( isset( $request[ Waf_Runner::SHARE_DATA_OPTION_NAME ] ) ) {
// If a user disabled the regular share we should disable the debug share data option.
if ( false === $request[ Waf_Runner::SHARE_DATA_OPTION_NAME ] ) {
update_option( Waf_Runner::SHARE_DEBUG_DATA_OPTION_NAME, false );
}
update_option( Waf_Runner::SHARE_DATA_OPTION_NAME, (bool) $request[ Waf_Runner::SHARE_DATA_OPTION_NAME ] );
}
// Share Debug Data
if ( isset( $request[ Waf_Runner::SHARE_DEBUG_DATA_OPTION_NAME ] ) ) {
// If a user toggles the debug share we should enable the regular share data option.
if ( true === $request[ Waf_Runner::SHARE_DEBUG_DATA_OPTION_NAME ] ) {
update_option( Waf_Runner::SHARE_DATA_OPTION_NAME, true );
}
update_option( Waf_Runner::SHARE_DEBUG_DATA_OPTION_NAME, (bool) $request[ Waf_Runner::SHARE_DEBUG_DATA_OPTION_NAME ] );
}
// Brute Force Protection
if ( isset( $request['brute_force_protection'] ) ) {
$enable_brute_force = (bool) $request['brute_force_protection'];

View File

@ -61,9 +61,10 @@ class Waf_Constants {
*/
public static function define_killswitch() {
if ( ! defined( 'DISABLE_JETPACK_WAF' ) ) {
$is_wpcom = defined( 'IS_WPCOM' ) && IS_WPCOM;
$is_atomic = ( new Host() )->is_atomic_platform();
define( 'DISABLE_JETPACK_WAF', $is_wpcom || $is_atomic );
$is_wpcom = defined( 'IS_WPCOM' ) && IS_WPCOM;
$is_atomic = ( new Host() )->is_atomic_platform();
$is_atomic_on_jn = defined( 'IS_ATOMIC_JN' ) ?? IS_ATOMIC_JN;
define( 'DISABLE_JETPACK_WAF', $is_wpcom || ( $is_atomic && ! $is_atomic_on_jn ) );
}
}
@ -86,9 +87,21 @@ class Waf_Constants {
*/
public static function define_share_data() {
if ( ! defined( 'JETPACK_WAF_SHARE_DATA' ) ) {
$share_data_option = get_option( Waf_Runner::SHARE_DATA_OPTION_NAME, false );
$share_data_option = false;
if ( function_exists( 'get_option' ) ) {
$share_data_option = get_option( Waf_Runner::SHARE_DATA_OPTION_NAME, false );
}
define( 'JETPACK_WAF_SHARE_DATA', $share_data_option );
}
if ( ! defined( 'JETPACK_WAF_SHARE_DEBUG_DATA' ) ) {
$share_debug_data_option = false;
if ( function_exists( 'get_option' ) ) {
$share_debug_data_option = get_option( Waf_Runner::SHARE_DEBUG_DATA_OPTION_NAME, false );
}
define( 'JETPACK_WAF_SHARE_DEBUG_DATA', $share_debug_data_option );
}
}
/**

View File

@ -279,7 +279,7 @@ class Waf_Operators {
return false;
}
return stripos( $test, $input ) !== false
return strpos( $test, $input ) !== false
? $input
: false;
}

View File

@ -146,6 +146,22 @@ class Waf_Request {
return $value;
}
/**
* Returns the value of a specific header that was sent with this request
*
* @param string $name The name of the header to retrieve.
* @return string
*/
public function get_header( $name ) {
$name = $this->normalize_header_name( $name );
foreach ( $this->get_headers() as list( $header_name, $header_value ) ) {
if ( $header_name === $name ) {
return $header_value;
}
}
return '';
}
/**
* Change a header name to all-lowercase and replace spaces and underscores with dashes.
*
@ -192,7 +208,9 @@ class Waf_Request {
$uri = isset( $_SERVER['REQUEST_URI'] ) ? filter_var( wp_unslash( $_SERVER['REQUEST_URI'] ), FILTER_DEFAULT ) : '/';
if ( false !== strpos( $uri, '?' ) ) {
// remove the query string (we'll pull it from elsewhere later)
$uri = substr( $uri, 0, strpos( $uri, '?' ) );
$uri = urldecode( substr( $uri, 0, strpos( $uri, '?' ) ) );
} else {
$uri = urldecode( $uri );
}
$query_string = isset( $_SERVER['QUERY_STRING'] ) ? '?' . filter_var( wp_unslash( $_SERVER['QUERY_STRING'] ), FILTER_DEFAULT ) : '';
if ( 1 === preg_match( '/^https?:\/\//', $uri ) ) {
@ -253,6 +271,24 @@ class Waf_Request {
return $this->get_url()[1];
}
/**
* Return the basename part of the request
*
* @example for 'https://wordpress.com/some/page.php?id=5', return 'page.php'
* @return string
*/
public function get_basename() {
// Get the filename part of the request
$filename = $this->get_filename();
// Normalize slashes
$filename = str_replace( '\\', '/', $filename );
// Remove trailing slashes
$filename = rtrim( $filename, '/' );
// Return the basename
$offset = strrpos( $filename, '/' );
return $offset !== false ? substr( $filename, $offset + 1 ) : $filename;
}
/**
* Return the query string. If present, it will be prefixed with '?'. Otherwise, it will be an empty string.
*
@ -296,6 +332,12 @@ class Waf_Request {
* @return array<string, mixed|array>
*/
public function get_post_vars() {
// Attempt to decode JSON requests.
if ( strpos( $this->get_header( 'content-type' ), 'application/json' ) !== false ) {
$decoded_json = json_decode( $this->get_body(), true ) ?? array();
return flatten_array( $decoded_json, 'json', true );
}
return flatten_array( $_POST );
}

View File

@ -16,9 +16,10 @@ use Automattic\Jetpack\Waf\Brute_Force_Protection\Brute_Force_Protection;
*/
class Waf_Runner {
const WAF_MODULE_NAME = 'waf';
const MODE_OPTION_NAME = 'jetpack_waf_mode';
const SHARE_DATA_OPTION_NAME = 'jetpack_waf_share_data';
const WAF_MODULE_NAME = 'waf';
const MODE_OPTION_NAME = 'jetpack_waf_mode';
const SHARE_DATA_OPTION_NAME = 'jetpack_waf_share_data';
const SHARE_DEBUG_DATA_OPTION_NAME = 'jetpack_waf_share_debug_data';
/**
* Run the WAF
@ -31,6 +32,7 @@ class Waf_Runner {
}
Waf_Constants::define_mode();
Waf_Constants::define_share_data();
if ( ! self::is_allowed_mode( JETPACK_WAF_MODE ) ) {
return;
}
@ -96,6 +98,10 @@ class Waf_Runner {
return false;
}
if ( defined( 'IS_ATOMIC_JN' ) && IS_ATOMIC_JN ) {
return true;
}
// Do not run in the WPCOM context
if ( ( new Host() )->is_wpcom_simple() ) {
return false;
@ -159,7 +165,9 @@ class Waf_Runner {
Waf_Rules_Manager::IP_ALLOW_LIST_OPTION_NAME => get_option( Waf_Rules_Manager::IP_ALLOW_LIST_OPTION_NAME ),
Waf_Rules_Manager::IP_BLOCK_LIST_OPTION_NAME => get_option( Waf_Rules_Manager::IP_BLOCK_LIST_OPTION_NAME ),
self::SHARE_DATA_OPTION_NAME => get_option( self::SHARE_DATA_OPTION_NAME ),
self::SHARE_DEBUG_DATA_OPTION_NAME => get_option( self::SHARE_DEBUG_DATA_OPTION_NAME ),
'bootstrap_path' => self::get_bootstrap_file_path(),
'standalone_mode' => self::get_standalone_mode_status(),
'automatic_rules_available' => (bool) self::automatic_rules_available(),
'brute_force_protection' => (bool) Brute_Force_Protection::is_enabled(),
);
@ -175,6 +183,15 @@ class Waf_Runner {
return $bootstrap->get_bootstrap_file_path();
}
/**
* Get WAF standalone mode status
*
* @return bool|array True if WAF standalone mode is enabled, false otherwise.
*/
public static function get_standalone_mode_status() {
return defined( 'JETPACK_WAF_RUN' ) && JETPACK_WAF_RUN === 'preload';
}
/**
* Get WAF File Path
*

View File

@ -278,6 +278,20 @@ class Waf_Runtime {
}
}
/**
* Get the headers for logging purposes.
*/
public function get_request_headers() {
$all_headers = getallheaders();
$exclude_headers = array( 'Authorization', 'Cookie', 'Proxy-Authorization', 'Set-Cookie' );
foreach ( $exclude_headers as $header ) {
unset( $all_headers[ $header ] );
}
return $all_headers;
}
/**
* Write block logs. We won't write to the file if it exceeds 100 mb.
*
@ -285,10 +299,20 @@ class Waf_Runtime {
* @param string $reason Block reason.
*/
public function write_blocklog( $rule_id, $reason ) {
$log_data = array();
$log_data['rule_id'] = $rule_id;
$log_data['reason'] = $reason;
$log_data['timestamp'] = gmdate( 'Y-m-d H:i:s' );
$log_data = array();
$log_data['rule_id'] = $rule_id;
$log_data['reason'] = $reason;
$log_data['timestamp'] = gmdate( 'Y-m-d H:i:s' );
$log_data['request_uri'] = isset( $_SERVER['REQUEST_URI'] ) ? \stripslashes( $_SERVER['REQUEST_URI'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash
$log_data['user_agent'] = isset( $_SERVER['HTTP_USER_AGENT'] ) ? \stripslashes( $_SERVER['HTTP_USER_AGENT'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash
$log_data['referer'] = isset( $_SERVER['HTTP_REFERER'] ) ? \stripslashes( $_SERVER['HTTP_REFERER'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash
$log_data['content_type'] = isset( $_SERVER['CONTENT_TYPE'] ) ? \stripslashes( $_SERVER['CONTENT_TYPE'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash
$log_data['get_params'] = json_encode( $_GET );
if ( defined( 'JETPACK_WAF_SHARE_DEBUG_DATA' ) && JETPACK_WAF_SHARE_DEBUG_DATA ) {
$log_data['post_params'] = json_encode( $_POST );
$log_data['headers'] = $this->get_request_headers();
}
if ( defined( 'JETPACK_WAF_SHARE_DATA' ) && JETPACK_WAF_SHARE_DATA ) {
$file_path = JETPACK_WAF_DIR . '/waf-blocklog';
@ -495,7 +519,7 @@ class Waf_Runtime {
);
break;
case 'request_basename':
$value = basename( $this->request->get_filename() );
$value = $this->request->get_basename();
break;
case 'request_body':
$value = $this->request->get_body();

View File

@ -140,9 +140,10 @@ class Waf_Standalone_Bootstrap {
$autoloader_file = $this->locate_autoloader_file();
$bootstrap_file = $this->get_bootstrap_file_path();
$mode_option = get_option( Waf_Runner::MODE_OPTION_NAME, false );
$share_data_option = get_option( Waf_Runner::SHARE_DATA_OPTION_NAME, false );
$bootstrap_file = $this->get_bootstrap_file_path();
$mode_option = get_option( Waf_Runner::MODE_OPTION_NAME, false );
$share_data_option = get_option( Waf_Runner::SHARE_DATA_OPTION_NAME, false );
$share_debug_data_option = get_option( Waf_Runner::SHARE_DEBUG_DATA_OPTION_NAME, false );
// phpcs:disable WordPress.PHP.DevelopmentFunctions
$code = "<?php\n"
@ -150,6 +151,7 @@ class Waf_Standalone_Bootstrap {
. "if ( defined( 'DISABLE_JETPACK_WAF' ) && DISABLE_JETPACK_WAF ) return;\n"
. sprintf( "define( 'JETPACK_WAF_MODE', %s );\n", var_export( $mode_option ? $mode_option : 'silent', true ) )
. sprintf( "define( 'JETPACK_WAF_SHARE_DATA', %s );\n", var_export( $share_data_option, true ) )
. sprintf( "define( 'JETPACK_WAF_SHARE_DEBUG_DATA', %s );\n", var_export( $share_debug_data_option, true ) )
. sprintf( "define( 'JETPACK_WAF_DIR', %s );\n", var_export( JETPACK_WAF_DIR, true ) )
. sprintf( "define( 'JETPACK_WAF_WPCONFIG', %s );\n", var_export( JETPACK_WAF_WPCONFIG, true ) )
. 'require_once ' . var_export( $autoloader_file, true ) . ";\n"

View File

@ -11,14 +11,33 @@ namespace Automattic\Jetpack\Waf;
* Waf_Transforms class
*/
class Waf_Transforms {
/**
* Decode a Base64-encoded string.
* Decode a Base64-encoded string. This runs the decode without strict mode, to match Modsecurity's 'base64DecodeExt' transform function.
*
* @param string $value value to be decoded.
* @return string
*/
public function base64_decode_ext( $value ) {
return base64_decode( $value );
}
/**
* Characters to match when trimming a string.
* Emulates `std::isspace` used by ModSecurity.
*
* @see https://en.cppreference.com/w/cpp/string/byte/isspace
*/
const TRIM_CHARS = " \n\r\t\v\f";
/**
* Decode a Base64-encoded string. This runs the decode with strict mode, to match Modsecurity's 'base64Decode' transform function.
*
* @param string $value value to be decoded.
* @return string
*/
public function base64_decode( $value ) {
return base64_decode( $value );
return base64_decode( $value, true );
}
/**
@ -307,7 +326,7 @@ class Waf_Transforms {
* @return string
*/
public function trim_left( $value ) {
return ltrim( $value );
return ltrim( $value, self::TRIM_CHARS );
}
/**
@ -317,7 +336,7 @@ class Waf_Transforms {
* @return string
*/
public function trim_right( $value ) {
return rtrim( $value );
return rtrim( $value, self::TRIM_CHARS );
}
/**
@ -327,16 +346,58 @@ class Waf_Transforms {
* @return string
*/
public function trim( $value ) {
return trim( $value );
return trim( $value, self::TRIM_CHARS );
}
/**
* Convert utf-8 characters to unicode characters
* Convert UTF-8 characters to Unicode characters.
*
* @param string $value value to be encoded.
* @return string
* This function iterates through each character of the input string, checks the ASCII value,
* and converts it to its corresponding Unicode representation. It handles characters that are
* represented with 1 to 4 bytes in UTF-8.
*
* @param string $str The string value to be encoded from UTF-8 to Unicode.
* @return string The converted string with Unicode representation.
*/
public function utf8_to_unicode( $value ) {
return preg_replace( '/\\\u(?=[a-f0-9]{4})/', '%u', substr( json_encode( $value ), 1, -1 ) );
public function utf8_to_unicode( $str ) {
$unicodeStr = '';
$strLen = strlen( $str );
$i = 0;
// Iterate through each character of the input string.
while ( $i < $strLen ) {
// Get the ASCII value of the current character.
$value = ord( $str[ $i ] );
if ( $value < 128 ) {
// If the character is in the ASCII range (0-127), directly add it to the Unicode string.
$unicodeStr .= chr( $value );
++$i;
} else {
// For characters outside the ASCII range, determine the number of bytes in the UTF-8 representation.
$unicodeValue = '';
if ( $value >= 192 && $value <= 223 ) {
// For characters represented with 2 bytes in UTF-8.
$unicodeValue = ( ord( $str[ $i ] ) & 0x1F ) << 6 | ( ord( $str[ $i + 1 ] ) & 0x3F );
$i += 2;
} elseif ( $value >= 224 && $value <= 239 ) {
// For characters represented with 3 bytes in UTF-8.
$unicodeValue = ( ord( $str[ $i ] ) & 0x0F ) << 12 | ( ord( $str[ $i + 1 ] ) & 0x3F ) << 6 | ( ord( $str[ $i + 2 ] ) & 0x3F );
$i += 3;
} elseif ( $value >= 240 && $value <= 247 ) {
// For characters represented with 4 bytes in UTF-8.
$unicodeValue = ( ord( $str[ $i ] ) & 0x07 ) << 18 | ( ord( $str[ $i + 1 ] ) & 0x3F ) << 12 | ( ord( $str[ $i + 2 ] ) & 0x3F ) << 6 | ( ord( $str[ $i + 3 ] ) & 0x3F );
$i += 4;
} else {
// If the sequence does not match any known UTF-8 pattern, skip to the next character.
++$i;
continue;
}
// Convert the Unicode value to a formatted string and append it to the Unicode string.
$unicodeStr .= sprintf( '%%u%04X', $unicodeValue );
}
}
return strtolower( $unicodeStr );
}
}

View File

@ -47,23 +47,25 @@ function wp_unslash( $value ) {
* [ "field2[2]", "f" ],
* ]
*
* @param array $array An array that resembles one of the PHP superglobals like $_GET or $_POST.
* @param string $key_prefix String that should be prepended to the keys output by this function.
* Usually only used internally as part of recursion when flattening a nested array.
* @param array $array An array that resembles one of the PHP superglobals like $_GET or $_POST.
* @param string $key_prefix String that should be prepended to the keys output by this function.
* Usually only used internally as part of recursion when flattening a nested array.
* @param bool|null $dot_notation Whether to use dot notation instead of bracket notation.
*
* @return array{ 0: string, 1: scalar }[] $key_prefix An array of key/value tuples, one for each distinct value in the input array.
*/
function flatten_array( $array, $key_prefix = '' ) {
function flatten_array( $array, $key_prefix = '', $dot_notation = null ) {
$return = array();
foreach ( $array as $source_key => $source_value ) {
$key = ( '' === $key_prefix )
// if this is the first level, the key name isn't enclosed in brackets
? $source_key
// for every level after the first, enclose the key name in brackets.
: $key_prefix . '[' . $source_key . ']';
$key = $source_key;
if ( ! empty( $key_prefix ) ) {
$key = $dot_notation ? "$key_prefix.$source_key" : $key_prefix . "[$source_key]";
}
if ( ! is_array( $source_value ) ) {
$return[] = array( $key, $source_value );
} else {
$return = array_merge( $return, flatten_array( $source_value, $key ) );
$return = array_merge( $return, flatten_array( $source_value, $key, $dot_notation ) );
}
}
return $return;