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: Allow rules to specify body parser type #39516

Merged
merged 4 commits into from
Oct 4, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
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 JSON via Waf_Request::get_post_vars().
nateweller marked this conversation as resolved.
Show resolved Hide resolved
*/
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
Loading