Skip to content

Commit 05a71ff

Browse files
committed
Security: allow for type testing superglobals
When a superglobal variable is being tested with, for instance, `is_numeric()`, unslashing or sanitization are not needed and it's ok for the nonce check to be done after it. This is completely safe. Ref: https://php.net/manual/en/ref.var.php Using these type testing functions should however not be regarded as a way of sanitizing/unslashing the variable and the variable does still need to be validated before being passed to one of these functions. To test whether a variable is used in one of these functions, a new `is_in_type_test()` method has been added to the `WordPressCS\Sniff` class, along with a `$typeTestFunctions` property containing the names of the functions this applies to. Notes: * The `is_array()` function which was previously, incorrectly, listed in the `$unslashingSanitizingFunctions` list has been moved to the new property. `is_array()` does not unslash or sanitize the contents of a variable, it only checks the variable type. * Implemented the use of the new `Sniff::is_in_type_test()` method in both the `ValidatedSanitizedInput` sniff, as well as in the `Sniff:has_nonce_check()` method for the `NonceVerification` sniff. Includes unit tests via the sniffs.
1 parent 33b55fe commit 05a71ff

File tree

6 files changed

+97
-8
lines changed

6 files changed

+97
-8
lines changed

WordPress/Sniff.php

Lines changed: 61 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -312,11 +312,42 @@ abstract class Sniff implements PHPCS_Sniff {
312312
'doubleval' => true,
313313
'floatval' => true,
314314
'intval' => true,
315-
'is_array' => true,
316315
'sanitize_key' => true,
317316
'sizeof' => true,
318317
);
319318

319+
/**
320+
* List of PHP native functions to test the type of a variable.
321+
*
322+
* Using these functions is safe in combination with superglobals without
323+
* unslashing or sanitization.
324+
*
325+
* They should, however, not be regarded as unslashing or sanitization functions.
326+
*
327+
* @since 2.1.0
328+
*
329+
* @var array
330+
*/
331+
protected $typeTestFunctions = array(
332+
'is_array' => true,
333+
'is_bool' => true,
334+
'is_callable' => true,
335+
'is_countable' => true,
336+
'is_double' => true,
337+
'is_float' => true,
338+
'is_int' => true,
339+
'is_integer' => true,
340+
'is_iterable' => true,
341+
'is_long' => true,
342+
'is_null' => true,
343+
'is_numeric' => true,
344+
'is_object' => true,
345+
'is_real' => true,
346+
'is_resource' => true,
347+
'is_scalar' => true,
348+
'is_string' => true,
349+
);
350+
320351
/**
321352
* Token which when they preceed code indicate the value is safely casted.
322353
*
@@ -1372,12 +1403,17 @@ protected function has_nonce_check( $stackPtr ) {
13721403
}
13731404
}
13741405

1375-
$in_isset = $this->is_in_isset_or_empty( $stackPtr );
1406+
$allow_nonce_after = false;
1407+
if ( $this->is_in_isset_or_empty( $stackPtr )
1408+
|| $this->is_in_type_test( $stackPtr )
1409+
) {
1410+
$allow_nonce_after = true;
1411+
}
13761412

1377-
// We allow for isset( $_POST['var'] ) checks to come before the nonce check.
1378-
// If this is inside an isset(), check after it as well, all the way to the
1379-
// end of the scope.
1380-
if ( $in_isset ) {
1413+
// We allow for certain actions, such as an isset() check to come before the nonce check.
1414+
// If this superglobal is inside such a check, look for the nonce after it as well,
1415+
// all the way to the end of the scope.
1416+
if ( true === $allow_nonce_after ) {
13811417
$end = ( 0 === $start ) ? $this->phpcsFile->numTokens : $tokens[ $start ]['scope_closer'];
13821418
}
13831419

@@ -1393,7 +1429,7 @@ protected function has_nonce_check( $stackPtr ) {
13931429
// If we have already found an nonce check in this scope, we just
13941430
// need to check whether it comes before this token. It is OK if the
13951431
// check is after the token though, if this was only a isset() check.
1396-
return ( $in_isset || $last['nonce_check'] < $stackPtr );
1432+
return ( true === $allow_nonce_after || $last['nonce_check'] < $stackPtr );
13971433
} elseif ( $end <= $last['end'] ) {
13981434
// If not, we can still go ahead and return false if we've already
13991435
// checked to the end of the search area.
@@ -1624,6 +1660,24 @@ protected function is_in_function_call( $stackPtr, $valid_functions, $global = t
16241660
return false;
16251661
}
16261662

1663+
/**
1664+
* Check if a token is inside of an is_...() statement.
1665+
*
1666+
* @since 2.1.0
1667+
*
1668+
* @param int $stackPtr The index of the token in the stack.
1669+
*
1670+
* @return bool Whether the token is being type tested.
1671+
*/
1672+
protected function is_in_type_test( $stackPtr ) {
1673+
/*
1674+
* Casting the potential integer stack pointer return value to boolean here is fine.
1675+
* The return can never be `0` as there will always be a PHP open tag before the
1676+
* function call.
1677+
*/
1678+
return (bool) $this->is_in_function_call( $stackPtr, $this->typeTestFunctions );
1679+
}
1680+
16271681
/**
16281682
* Check if something is only being sanitized.
16291683
*

WordPress/Sniffs/Security/ValidatedSanitizedInputSniff.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,11 @@ function ( $symbol ) {
145145
return;
146146
}
147147

148+
// If this variable is being tested with one of the `is_..()` functions, sanitization isn't needed.
149+
if ( $this->is_in_type_test( $stackPtr ) ) {
150+
return;
151+
}
152+
148153
// If this is a comparison ('a' == $_POST['foo']), sanitization isn't needed.
149154
if ( $this->is_comparison( $stackPtr ) ) {
150155
return;

WordPress/Tests/Security/NonceVerificationUnitTest.inc

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ function ajax_process() {
1717
}
1818
add_action( 'wp_ajax_process', 'ajax_process' );
1919

20-
// It's also OK to check with isset() before the the nonce check.
20+
// It's also OK to check with isset() before the nonce check.
2121
function foo() {
2222
if ( ! isset( $_POST['test'] ) || ! wp_verify_nonce( 'some_action' ) ) {
2323
exit;
@@ -160,3 +160,21 @@ function foo_8() {
160160
sanitize_pc( $_POST['bar'] ); // Bad.
161161
my_nonce_check( sanitize_twitter( $_POST['tweet'] ) ); // Bad.
162162
}
163+
164+
/*
165+
* Using a superglobal in a is_...() function is OK as long as a nonce check is done
166+
* before the variable is *really* used.
167+
*/
168+
function test_whitelisting_use_in_type_test_functions() {
169+
if ( ! is_numeric ( $_POST['foo'] ) ) { // OK.
170+
return;
171+
}
172+
173+
wp_verify_nonce( 'some_action' );
174+
}
175+
176+
function test_incorrect_use_in_type_test_functions() {
177+
if ( ! is_numeric ( $_POST['foo'] ) ) { // Bad.
178+
return;
179+
}
180+
}

WordPress/Tests/Security/NonceVerificationUnitTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ public function getErrorList() {
4646
159 => 1,
4747
160 => 1,
4848
161 => 1,
49+
177 => 1,
4950
);
5051
}
5152

WordPress/Tests/Security/ValidatedSanitizedInputUnitTest.inc

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,3 +257,13 @@ function test_invalid_array_key_exists_call() {
257257
$id = (int) $_POST['bar']; // Bad x 1 - validate.
258258
}
259259
}
260+
261+
function test_ignoring_is_type_function_calls() {
262+
// Usage within variable type tests does not need unslashing or sanitization.
263+
if ( isset( $_POST[ $key ] ) && is_numeric( $_POST[ $key ] ) ) {} // OK.
264+
if ( isset($_POST['plugin']) && is_string( $_POST['plugin'] ) ) {} // OK.
265+
266+
if ( ! is_null( $_GET['not_set'] ) ) {} // Bad x1 - missing validation.
267+
if ( array_key_exists( 'null', $_GET ) && ! is_null( $_GET['null'] ) ) {} // OK.
268+
if ( array_key_exists( 'null', $_POST ) && $_POST['null'] !== null ) {} // OK.
269+
}

WordPress/Tests/Security/ValidatedSanitizedInputUnitTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ public function getErrorList() {
6565
245 => 1,
6666
251 => 1,
6767
257 => 1,
68+
266 => 1,
6869
);
6970
}
7071

0 commit comments

Comments
 (0)