Skip to content

Commit

Permalink
WAF: Allow rules to specify body parser type (#39516)
Browse files Browse the repository at this point in the history
* WAF: Add support for rules file to manually specify body parsing method

* Add public $version property to Waf_Runtime
  • Loading branch information
nateweller authored Oct 4, 2024
1 parent a58dbad commit d6d3ba7
Show file tree
Hide file tree
Showing 4 changed files with 143 additions and 14 deletions.
4 changes: 4 additions & 0 deletions projects/packages/waf/changelog/add-waf-body-processor
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: added

Firewall Runtime: Added support for rule files to specify body parser type.
57 changes: 44 additions & 13 deletions projects/packages/waf/src/class-waf-request.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

/**
Expand Down
38 changes: 37 additions & 1 deletion projects/packages/waf/src/class-waf-runtime.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -68,6 +76,12 @@ class Waf_Runtime {
* @var string
*/
public $matched_var_name = '';
/**
* Body Processor.
*
* @var string 'URLENCODED' | 'JSON' | ''
*/
private $body_processor = '';

/**
* State.
Expand Down Expand Up @@ -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' ) );
Expand Down Expand Up @@ -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.
*
Expand Down
58 changes: 58 additions & 0 deletions projects/packages/waf/tests/php/unit/test-waf-request.php
Original file line number Diff line number Diff line change
Expand Up @@ -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().
*/
Expand Down

0 comments on commit d6d3ba7

Please sign in to comment.