diff --git a/projects/packages/waf/changelog/add-waf-body-processor b/projects/packages/waf/changelog/add-waf-body-processor new file mode 100644 index 0000000000000..df675355a4f53 --- /dev/null +++ b/projects/packages/waf/changelog/add-waf-body-processor @@ -0,0 +1,4 @@ +Significance: minor +Type: added + +Firewall Runtime: Added support for rule files to specify body parser type. diff --git a/projects/packages/waf/src/class-waf-request.php b/projects/packages/waf/src/class-waf-request.php index 5017a2e25f46c..bbb8bcf3e17f9 100644 --- a/projects/packages/waf/src/class-waf-request.php +++ b/projects/packages/waf/src/class-waf-request.php @@ -328,28 +328,59 @@ public function get_get_vars() { return flatten_array( $_GET ); } + /** + * Returns the POST variables from a JSON body + * + * @return array{string, scalar}[] + */ + private function get_json_post_vars() { + $decoded_json = json_decode( $this->get_body(), true ) ?? array(); + return flatten_array( $decoded_json, 'json', true ); + } + + /** + * Returns the POST variables from a urlencoded body + * + * @return array{string, scalar}[] + */ + private function get_urlencoded_post_vars() { + parse_str( $this->get_body(), $params ); + return flatten_array( $params ); + } + /** * Returns the POST variables * + * @param string $body_processor Manually specifiy the method to use to process the body. Options are 'URLENCODED' and 'JSON'. + * * @return array{string, scalar}[] */ - public function get_post_vars() { + public function get_post_vars( string $body_processor = '' ) { $content_type = $this->get_header( 'content-type' ); + + // If the body processor is specified by the rules file, trust it. + if ( 'URLENCODED' === $body_processor ) { + return $this->get_urlencoded_post_vars(); + } + if ( 'JSON' === $body_processor ) { + return $this->get_json_post_vars(); + } + + // Otherwise, use $_POST if it's not empty. if ( ! empty( $_POST ) ) { - // If $_POST is populated, use it. return flatten_array( $_POST ); - } elseif ( strpos( $content_type, 'application/json' ) !== false ) { - // Attempt to decode JSON requests. - $decoded_json = json_decode( $this->get_body(), true ) ?? array(); - return flatten_array( $decoded_json, 'json', true ); - } elseif ( strpos( $content_type, 'application/x-www-form-urlencoded' ) !== false ) { - // Attempt to decode url-encoded data - parse_str( $this->get_body(), $params ); - return flatten_array( $params ); - } else { - // Don't try to parse any other content types - return array(); } + + // Lastly, try to parse the body based on the content type. + if ( strpos( $content_type, 'application/json' ) !== false ) { + return $this->get_json_post_vars(); + } + if ( strpos( $content_type, 'application/x-www-form-urlencoded' ) !== false ) { + return $this->get_urlencoded_post_vars(); + } + + // Don't try to parse any other content types. + return array(); } /** diff --git a/projects/packages/waf/src/class-waf-runtime.php b/projects/packages/waf/src/class-waf-runtime.php index 394d46aa5a6f1..19300ea804582 100644 --- a/projects/packages/waf/src/class-waf-runtime.php +++ b/projects/packages/waf/src/class-waf-runtime.php @@ -38,6 +38,14 @@ class Waf_Runtime { */ const NORMALIZE_ARRAY_MATCH_VALUES = 2; + /** + * The version of this runtime class. Used by rule files to ensure compatibility. + * + * @since $$next-version$$ + * + * @var int + */ + public $version = 1; /** * Last rule. * @@ -68,6 +76,12 @@ class Waf_Runtime { * @var string */ public $matched_var_name = ''; + /** + * Body Processor. + * + * @var string 'URLENCODED' | 'JSON' | '' + */ + private $body_processor = ''; /** * State. @@ -438,7 +452,7 @@ public function meta( $key ) { $value = $this->args_names( $this->meta( 'args_get' ) ); break; case 'args_post': - $value = $this->request->get_post_vars(); + $value = $this->request->get_post_vars( $this->get_body_processor() ); break; case 'args_post_names': $value = $this->args_names( $this->meta( 'args_post' ) ); @@ -488,6 +502,28 @@ private function state_values( $prefix ) { return $output; } + /** + * Get the body processor. + * + * @return string + */ + private function get_body_processor() { + return $this->body_processor; + } + + /** + * Set the body processor. + * + * @param string $processor Processor to set. Either 'URLENCODED' or 'JSON'. + * + * @return void + */ + public function set_body_processor( $processor ) { + if ( $processor === 'URLENCODED' || $processor === 'JSON' ) { + $this->body_processor = $processor; + } + } + /** * Change a string to all lowercase and replace spaces and underscores with dashes. * diff --git a/projects/packages/waf/tests/php/unit/test-waf-request.php b/projects/packages/waf/tests/php/unit/test-waf-request.php index ce409a4b9e5ac..01c68e5aa5bea 100644 --- a/projects/packages/waf/tests/php/unit/test-waf-request.php +++ b/projects/packages/waf/tests/php/unit/test-waf-request.php @@ -301,6 +301,64 @@ public function testGetVarsPost() { $_POST = array(); } + /** + * Test that the Waf_Request class returns POST-ed data correctly decoded from JSON via Waf_Request::get_post_vars(). + */ + public function testGetVarsPostWithJsonBodyProcessor() { + $_SERVER['CONTENT_TYPE'] = 'irrelevant'; + + $request = $this->mock_request( + array( + 'body' => json_encode( + array( + 'str' => 'value', + 'arr' => array( 'a', 'b', 'c' ), + 'obj' => (object) array( 'foo' => 'bar' ), + ) + ), + ) + ); + $value = $request->get_post_vars( 'JSON' ); + $this->assertIsArray( $value ); + $this->assertContains( array( 'json.str', 'value' ), $value ); + $this->assertContains( array( 'json.arr.0', 'a' ), $value ); + $this->assertContains( array( 'json.arr.1', 'b' ), $value ); + $this->assertContains( array( 'json.arr.2', 'c' ), $value ); + $this->assertContains( array( 'json.obj.foo', 'bar' ), $value ); + + unset( $_SERVER['CONTENT_TYPE'] ); + } + + /** + * Test that the Waf_Request class returns POST-ed data correctly decoded from URLENCODED body via Waf_Request::get_post_vars(). + */ + public function testGetVarsPostWithUrlencodedBodyProcessor() { + $_SERVER['CONTENT_TYPE'] = 'irrelevant'; + + $request = $this->mock_request( + array( + 'body' => ( + http_build_query( + array( + 'str' => 'value', + 'arr' => array( 'a', 'b', 'c' ), + 'obj' => (object) array( 'foo' => 'bar' ), + ) + ) + ), + ) + ); + $value = $request->get_post_vars( 'URLENCODED' ); + $this->assertIsArray( $value ); + $this->assertContains( array( 'str', 'value' ), $value ); + $this->assertContains( array( 'arr[0]', 'a' ), $value ); + $this->assertContains( array( 'arr[1]', 'b' ), $value ); + $this->assertContains( array( 'arr[2]', 'c' ), $value ); + $this->assertContains( array( 'obj[foo]', 'bar' ), $value ); + + unset( $_SERVER['CONTENT_TYPE'] ); + } + /** * Test that the Waf_Request class returns POST-ed data correctly decoded from JSON via Waf_Request::get_post_vars(). */