Skip to content

Commit 90cc817

Browse files
authored
Merge pull request #1635 from WordPress-Coding-Standards/feature/sniff-isset-empty-add-array-key-exists
ValidateSanitizedInput: allow for validation using array_key_exists()
2 parents 913f9cc + 5f3905a commit 90cc817

File tree

3 files changed

+141
-21
lines changed

3 files changed

+141
-21
lines changed

WordPress/Sniff.php

Lines changed: 117 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1428,9 +1428,11 @@ protected function has_nonce_check( $stackPtr ) {
14281428
}
14291429

14301430
/**
1431-
* Check if a token is inside of an isset() or empty() statement.
1431+
* Check if a token is inside of an isset(), empty() or array_key_exists() statement.
14321432
*
14331433
* @since 0.5.0
1434+
* @since 2.0.1 Now checks for the token being used as the array parameter
1435+
* in function calls to array_key_exists() as well.
14341436
*
14351437
* @param int $stackPtr The index of the token in the stack.
14361438
*
@@ -1448,7 +1450,42 @@ protected function is_in_isset_or_empty( $stackPtr ) {
14481450
$open_parenthesis = key( $nested_parenthesis );
14491451

14501452
$previous_non_empty = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, ( $open_parenthesis - 1 ), null, true, null, true );
1451-
return in_array( $this->tokens[ $previous_non_empty ]['code'], array( \T_ISSET, \T_EMPTY ), true );
1453+
if ( false === $previous_non_empty ) {
1454+
return false;
1455+
}
1456+
1457+
$previous_code = $this->tokens[ $previous_non_empty ]['code'];
1458+
if ( \T_ISSET === $previous_code || \T_EMPTY === $previous_code ) {
1459+
return true;
1460+
}
1461+
1462+
if ( \T_STRING === $previous_code && 'array_key_exists' === $this->tokens[ $previous_non_empty ]['content'] ) {
1463+
$before_function = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, ( $previous_non_empty - 1 ), null, true, null, true );
1464+
1465+
if ( false !== $before_function ) {
1466+
if ( \T_OBJECT_OPERATOR === $this->tokens[ $before_function ]['code']
1467+
|| \T_DOUBLE_COLON === $this->tokens[ $before_function ]['code']
1468+
) {
1469+
// Method call.
1470+
return false;
1471+
}
1472+
1473+
if ( \T_NS_SEPARATOR === $this->tokens[ $before_function ]['code'] ) {
1474+
$before_before = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, ( $before_function - 1 ), null, true, null, true );
1475+
if ( false !== $before_before && \T_STRING === $this->tokens[ $before_before ]['code'] ) {
1476+
// Namespaced function call.
1477+
return false;
1478+
}
1479+
}
1480+
}
1481+
1482+
$second_param = $this->get_function_call_parameter( $previous_non_empty, 2 );
1483+
if ( $stackPtr >= $second_param['start'] && $stackPtr <= $second_param['end'] ) {
1484+
return true;
1485+
}
1486+
}
1487+
1488+
return false;
14521489
}
14531490

14541491
/**
@@ -1666,7 +1703,7 @@ protected function get_array_access_key( $stackPtr ) {
16661703
}
16671704

16681705
/**
1669-
* Check if the existence of a variable is validated with isset() or empty().
1706+
* Check if the existence of a variable is validated with isset(), empty() or array_key_exists().
16701707
*
16711708
* When $in_condition_only is false, (which is the default), this is considered
16721709
* valid:
@@ -1689,6 +1726,7 @@ protected function get_array_access_key( $stackPtr ) {
16891726
* ```
16901727
*
16911728
* @since 0.5.0
1729+
* @since 2.0.1 Now recognizes array_key_exists() as a validation function.
16921730
*
16931731
* @param int $stackPtr The index of this token in the stack.
16941732
* @param string $array_key An array key to check for ("bar" in $foo['bar']).
@@ -1754,36 +1792,94 @@ protected function is_validated( $stackPtr, $array_key = null, $in_condition_onl
17541792
}
17551793

17561794
$bare_array_key = $this->strip_quotes( $array_key );
1795+
$targets = array(
1796+
\T_ISSET => 'construct',
1797+
\T_EMPTY => 'construct',
1798+
\T_UNSET => 'construct',
1799+
\T_STRING => 'function_call',
1800+
);
17571801

17581802
// phpcs:ignore Generic.CodeAnalysis.JumbledIncrementer.Found -- On purpose, see below.
17591803
for ( $i = ( $scope_start + 1 ); $i < $scope_end; $i++ ) {
17601804

1761-
if ( ! \in_array( $this->tokens[ $i ]['code'], array( \T_ISSET, \T_EMPTY, \T_UNSET ), true ) ) {
1805+
if ( isset( $targets[ $this->tokens[ $i ]['code'] ] ) === false ) {
17621806
continue;
17631807
}
17641808

1765-
$issetOpener = $this->phpcsFile->findNext( \T_OPEN_PARENTHESIS, $i );
1766-
$issetCloser = $this->tokens[ $issetOpener ]['parenthesis_closer'];
1809+
switch ( $targets[ $this->tokens[ $i ]['code'] ] ) {
1810+
case 'construct':
1811+
$issetOpener = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $i + 1 ), null, true, null, true );
1812+
if ( false === $issetOpener || \T_OPEN_PARENTHESIS !== $this->tokens[ $issetOpener ]['code'] ) {
1813+
// Parse error or live coding.
1814+
continue 2;
1815+
}
17671816

1768-
// Look for this variable. We purposely stomp $i from the parent loop.
1769-
for ( $i = ( $issetOpener + 1 ); $i < $issetCloser; $i++ ) {
1817+
$issetCloser = $this->tokens[ $issetOpener ]['parenthesis_closer'];
17701818

1771-
if ( \T_VARIABLE !== $this->tokens[ $i ]['code'] ) {
1772-
continue;
1773-
}
1819+
// Look for this variable. We purposely stomp $i from the parent loop.
1820+
for ( $i = ( $issetOpener + 1 ); $i < $issetCloser; $i++ ) {
17741821

1775-
if ( $this->tokens[ $stackPtr ]['content'] !== $this->tokens[ $i ]['content'] ) {
1776-
continue;
1777-
}
1822+
if ( \T_VARIABLE !== $this->tokens[ $i ]['code'] ) {
1823+
continue;
1824+
}
17781825

1779-
// If we're checking for a specific array key (ex: 'hello' in
1780-
// $_POST['hello']), that must match too. Quote-style, however, doesn't matter.
1781-
if ( isset( $array_key )
1782-
&& $this->strip_quotes( $this->get_array_access_key( $i ) ) !== $bare_array_key ) {
1783-
continue;
1784-
}
1826+
if ( $this->tokens[ $stackPtr ]['content'] !== $this->tokens[ $i ]['content'] ) {
1827+
continue;
1828+
}
17851829

1786-
return true;
1830+
// If we're checking for a specific array key (ex: 'hello' in
1831+
// $_POST['hello']), that must match too. Quote-style, however, doesn't matter.
1832+
if ( isset( $array_key )
1833+
&& $this->strip_quotes( $this->get_array_access_key( $i ) ) !== $bare_array_key ) {
1834+
continue;
1835+
}
1836+
1837+
return true;
1838+
}
1839+
1840+
break;
1841+
1842+
case 'function_call':
1843+
// Only check calls to array_key_exists().
1844+
if ( 'array_key_exists' !== $this->tokens[ $i ]['content'] ) {
1845+
continue 2;
1846+
}
1847+
1848+
$next_non_empty = $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $i + 1 ), null, true, null, true );
1849+
if ( false === $next_non_empty || \T_OPEN_PARENTHESIS !== $this->tokens[ $next_non_empty ]['code'] ) {
1850+
// Not a function call.
1851+
continue 2;
1852+
}
1853+
1854+
$previous_non_empty = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, ( $i - 1 ), null, true, null, true );
1855+
if ( false !== $previous_non_empty ) {
1856+
if ( \T_OBJECT_OPERATOR === $this->tokens[ $previous_non_empty ]['code']
1857+
|| \T_DOUBLE_COLON === $this->tokens[ $previous_non_empty ]['code']
1858+
) {
1859+
// Method call.
1860+
continue 2;
1861+
}
1862+
1863+
if ( \T_NS_SEPARATOR === $this->tokens[ $previous_non_empty ]['code'] ) {
1864+
$pprev = $this->phpcsFile->findPrevious( Tokens::$emptyTokens, ( $previous_non_empty - 1 ), null, true, null, true );
1865+
if ( false !== $pprev && \T_STRING === $this->tokens[ $pprev ]['code'] ) {
1866+
// Namespaced function call.
1867+
continue 2;
1868+
}
1869+
}
1870+
}
1871+
1872+
$params = $this->get_function_call_parameters( $i );
1873+
if ( $params[2]['raw'] !== $this->tokens[ $stackPtr ]['content'] ) {
1874+
continue 2;
1875+
}
1876+
1877+
if ( isset( $array_key )
1878+
&& $this->strip_quotes( $params[1]['raw'] ) !== $bare_array_key ) {
1879+
continue 2;
1880+
}
1881+
1882+
return true;
17871883
}
17881884
}
17891885

WordPress/Tests/Security/ValidatedSanitizedInputUnitTest.inc

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,3 +188,24 @@ if ( isset( $_POST[ 'currentid' ] ) ){
188188
if ( isset ( $_POST['thisisnotget'] ) ) {
189189
$get = (int) $_GET['thisisnotget']; // Bad.
190190
}
191+
192+
// Recognize PHP native array_key_exists() as validation function.
193+
if ( array_key_exists( 'my_field1', $_POST ) ) {
194+
$id = (int) $_POST['my_field1']; // OK.
195+
}
196+
197+
if ( \array_key_exists( 'my_field2', $_POST ) ) {
198+
$id = (int) $_POST['my_field2']; // OK.
199+
}
200+
201+
if ( \Some\ClassName\array_key_exists( 'my_field3', $_POST ) ) {
202+
$id = (int) $_POST['my_field3']; // Bad.
203+
}
204+
205+
if ( $obj->array_key_exists( 'my_field4', $_POST ) ) {
206+
$id = (int) $_POST['my_field4']; // Bad.
207+
}
208+
209+
if ( ClassName::array_key_exists( 'my_field5', $_POST ) ) {
210+
$id = (int) $_POST['my_field5']; // Bad.
211+
}

WordPress/Tests/Security/ValidatedSanitizedInputUnitTest.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ public function getErrorList() {
5555
150 => 2,
5656
160 => 2,
5757
189 => 1,
58+
202 => 1,
59+
206 => 1,
60+
210 => 1,
5861
);
5962
}
6063

0 commit comments

Comments
 (0)