Skip to content

Commit 33b55fe

Browse files
authored
Merge pull request #1676 from WordPress-Coding-Standards/feature/validatedsanitizedinput-multi-level-array-key-check
ValidatedSanitizedInput/Sniff::is_validated(): allow for multi-level array key check
2 parents 1a2b777 + a598b08 commit 33b55fe

File tree

4 files changed

+152
-35
lines changed

4 files changed

+152
-35
lines changed

WordPress/Sniff.php

Lines changed: 117 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1814,38 +1814,80 @@ public function add_unslash_error( $stackPtr ) {
18141814
);
18151815
}
18161816

1817+
/**
1818+
* Get the index keys of an array variable.
1819+
*
1820+
* E.g., "bar" and "baz" in $foo['bar']['baz'].
1821+
*
1822+
* @since 2.1.0
1823+
*
1824+
* @param int $stackPtr The index of the variable token in the stack.
1825+
* @param bool $all Whether to get all keys or only the first.
1826+
* Defaults to `true`(= all).
1827+
*
1828+
* @return array An array of index keys whose value is being accessed.
1829+
* or an empty array if this is not array access.
1830+
*/
1831+
protected function get_array_access_keys( $stackPtr, $all = true ) {
1832+
1833+
$keys = array();
1834+
1835+
if ( \T_VARIABLE !== $this->tokens[ $stackPtr ]['code'] ) {
1836+
return $keys;
1837+
}
1838+
1839+
$current = $stackPtr;
1840+
1841+
do {
1842+
// Find the next non-empty token.
1843+
$open_bracket = $this->phpcsFile->findNext(
1844+
Tokens::$emptyTokens,
1845+
( $current + 1 ),
1846+
null,
1847+
true
1848+
);
1849+
1850+
// If it isn't a bracket, this isn't an array-access.
1851+
if ( false === $open_bracket
1852+
|| \T_OPEN_SQUARE_BRACKET !== $this->tokens[ $open_bracket ]['code']
1853+
|| ! isset( $this->tokens[ $open_bracket ]['bracket_closer'] )
1854+
) {
1855+
break;
1856+
}
1857+
1858+
$key = $this->phpcsFile->getTokensAsString(
1859+
( $open_bracket + 1 ),
1860+
( $this->tokens[ $open_bracket ]['bracket_closer'] - $open_bracket - 1 )
1861+
);
1862+
1863+
$keys[] = trim( $key );
1864+
$current = $this->tokens[ $open_bracket ]['bracket_closer'];
1865+
} while ( isset( $this->tokens[ $current ] ) && true === $all );
1866+
1867+
return $keys;
1868+
}
1869+
18171870
/**
18181871
* Get the index key of an array variable.
18191872
*
18201873
* E.g., "bar" in $foo['bar'].
18211874
*
18221875
* @since 0.5.0
1876+
* @since 2.1.0 Now uses get_array_access_keys() under the hood.
18231877
*
18241878
* @param int $stackPtr The index of the token in the stack.
18251879
*
18261880
* @return string|false The array index key whose value is being accessed.
18271881
*/
18281882
protected function get_array_access_key( $stackPtr ) {
18291883

1830-
// Find the next non-empty token.
1831-
$open_bracket = $this->phpcsFile->findNext(
1832-
Tokens::$emptyTokens,
1833-
( $stackPtr + 1 ),
1834-
null,
1835-
true
1836-
);
1884+
$keys = $this->get_array_access_keys( $stackPtr, false );
18371885

1838-
// If it isn't a bracket, this isn't an array-access.
1839-
if ( false === $open_bracket || \T_OPEN_SQUARE_BRACKET !== $this->tokens[ $open_bracket ]['code'] ) {
1840-
return false;
1886+
if ( isset( $keys[0] ) ) {
1887+
return $keys[0];
18411888
}
18421889

1843-
$key = $this->phpcsFile->getTokensAsString(
1844-
( $open_bracket + 1 ),
1845-
( $this->tokens[ $open_bracket ]['bracket_closer'] - $open_bracket - 1 )
1846-
);
1847-
1848-
return trim( $key );
1890+
return false;
18491891
}
18501892

18511893
/**
@@ -1874,17 +1916,20 @@ protected function get_array_access_key( $stackPtr ) {
18741916
*
18751917
* @since 0.5.0
18761918
* @since 2.1.0 Now recognizes array_key_exists() and key_exists() as validation functions.
1919+
* @since 2.1.0 Stricter check on whether the correct variable and the correct
1920+
* array keys are being validated.
18771921
*
1878-
* @param int $stackPtr The index of this token in the stack.
1879-
* @param string $array_key An array key to check for ("bar" in $foo['bar']).
1880-
* @param bool $in_condition_only Whether to require that this use of the
1881-
* variable occur within the scope of the
1882-
* validating condition, or just in the same
1883-
* scope as it (default).
1922+
* @param int $stackPtr The index of this token in the stack.
1923+
* @param array|string $array_keys An array key to check for ("bar" in $foo['bar'])
1924+
* or an array of keys for multi-level array access.
1925+
* @param bool $in_condition_only Whether to require that this use of the
1926+
* variable occur within the scope of the
1927+
* validating condition, or just in the same
1928+
* scope as it (default).
18841929
*
18851930
* @return bool Whether the var is validated.
18861931
*/
1887-
protected function is_validated( $stackPtr, $array_key = null, $in_condition_only = false ) {
1932+
protected function is_validated( $stackPtr, $array_keys = array(), $in_condition_only = false ) {
18881933

18891934
if ( $in_condition_only ) {
18901935
/*
@@ -1935,11 +1980,14 @@ protected function is_validated( $stackPtr, $array_key = null, $in_condition_onl
19351980
}
19361981

19371982
$scope_end = $stackPtr;
1983+
}
19381984

1985+
if ( ! empty( $array_keys ) && ! is_array( $array_keys ) ) {
1986+
$array_keys = (array) $array_keys;
19391987
}
19401988

1941-
$bare_array_key = $this->strip_quotes( $array_key );
1942-
$targets = array(
1989+
$bare_array_keys = array_map( array( $this, 'strip_quotes' ), $array_keys );
1990+
$targets = array(
19431991
\T_ISSET => 'construct',
19441992
\T_EMPTY => 'construct',
19451993
\T_UNSET => 'construct',
@@ -1974,11 +2022,15 @@ protected function is_validated( $stackPtr, $array_key = null, $in_condition_onl
19742022
continue;
19752023
}
19762024

1977-
// If we're checking for a specific array key (ex: 'hello' in
2025+
// If we're checking for specific array keys (ex: 'hello' in
19782026
// $_POST['hello']), that must match too. Quote-style, however, doesn't matter.
1979-
if ( isset( $array_key )
1980-
&& $this->strip_quotes( $this->get_array_access_key( $i ) ) !== $bare_array_key ) {
1981-
continue;
2027+
if ( ! empty( $bare_array_keys ) ) {
2028+
$found_keys = $this->get_array_access_keys( $i );
2029+
$found_keys = array_map( array( $this, 'strip_quotes' ), $found_keys );
2030+
$diff = array_diff_assoc( $bare_array_keys, $found_keys );
2031+
if ( ! empty( $diff ) ) {
2032+
continue;
2033+
}
19822034
}
19832035

19842036
return true;
@@ -2011,12 +2063,45 @@ protected function is_validated( $stackPtr, $array_key = null, $in_condition_onl
20112063
}
20122064

20132065
$params = $this->get_function_call_parameters( $i );
2014-
if ( $params[2]['raw'] !== $this->tokens[ $stackPtr ]['content'] ) {
2066+
if ( count( $params ) < 2 ) {
2067+
continue 2;
2068+
}
2069+
2070+
$param2_first_token = $this->phpcsFile->findNext( Tokens::$emptyTokens, $params[2]['start'], ( $params[2]['end'] + 1 ), true );
2071+
if ( false === $param2_first_token
2072+
|| \T_VARIABLE !== $this->tokens[ $param2_first_token ]['code']
2073+
|| $this->tokens[ $param2_first_token ]['content'] !== $this->tokens[ $stackPtr ]['content']
2074+
) {
20152075
continue 2;
20162076
}
20172077

2018-
if ( isset( $array_key )
2019-
&& $this->strip_quotes( $params[1]['raw'] ) !== $bare_array_key ) {
2078+
if ( ! empty( $bare_array_keys ) ) {
2079+
// Prevent the original array from being altered.
2080+
$bare_keys = $bare_array_keys;
2081+
$last_key = array_pop( $bare_keys );
2082+
2083+
/*
2084+
* For multi-level array access, the complete set of keys could be split between
2085+
* the first and the second parameter, but could also be completely in the second
2086+
* parameter, so we need to check both options.
2087+
*/
2088+
2089+
$found_keys = $this->get_array_access_keys( $param2_first_token );
2090+
$found_keys = array_map( array( $this, 'strip_quotes' ), $found_keys );
2091+
2092+
// First try matching the complete set against the second parameter.
2093+
$diff = array_diff_assoc( $bare_array_keys, $found_keys );
2094+
if ( empty( $diff ) ) {
2095+
return true;
2096+
}
2097+
2098+
// If that failed, try getting an exact match for the subset against the
2099+
// second parameter and the last key against the first.
2100+
if ( $bare_keys === $found_keys && $this->strip_quotes( $params[1]['raw'] ) === $last_key ) {
2101+
return true;
2102+
}
2103+
2104+
// Didn't find the correct array keys.
20202105
continue 2;
20212106
}
20222107

WordPress/Sniffs/Security/ValidatedSanitizedInputSniff.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,16 +123,16 @@ function ( $symbol ) {
123123
return;
124124
}
125125

126-
$array_key = $this->get_array_access_key( $stackPtr );
126+
$array_keys = $this->get_array_access_keys( $stackPtr );
127127

128-
if ( empty( $array_key ) ) {
128+
if ( empty( $array_keys ) ) {
129129
return;
130130
}
131131

132132
$error_data = array( $this->tokens[ $stackPtr ]['content'] );
133133

134134
// Check for validation first.
135-
if ( ! $this->is_validated( $stackPtr, $array_key, $this->check_validation_in_scope_only ) ) {
135+
if ( ! $this->is_validated( $stackPtr, $array_keys, $this->check_validation_in_scope_only ) ) {
136136
$this->phpcsFile->addError(
137137
'Detected usage of a non-validated input variable: %s',
138138
$stackPtr,

WordPress/Tests/Security/ValidatedSanitizedInputUnitTest.inc

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,3 +230,30 @@ function test_allow_array_key_exists_alias() {
230230
if ( key_exists( 'my_field1', $_POST ) ) {
231231
$id = (int) $_POST['my_field1']; // OK.
232232
}
233+
234+
function test_correct_multi_level_array_validation() {
235+
if ( isset( $_POST['toplevel']['sublevel'] ) ) {
236+
$id = (int) $_POST['toplevel']; // OK, if a subkey exists, the top level key *must* also exist.
237+
$id = (int) $_POST['toplevel']['sublevel']; // OK.
238+
$id = (int) $_POST['toplevel']['sublevel']['subsub']; // Bad x1 - validate, this sub has not been validated.
239+
}
240+
241+
if ( array_key_exists( 'bar', $_POST['foo'] ) ) {
242+
$id = (int) $_POST['bar']; // Bad x 1 - validate.
243+
$id = (int) $_POST['foo']; // OK.
244+
$id = (int) $_POST['foo']['bar']; // OK.
245+
$id = (int) $_POST['foo']['bar']['baz']; // Bad x 1 - validate.
246+
}
247+
}
248+
249+
function test_correct_multi_level_array_validation_key_order() {
250+
if ( isset( $_POST['toplevel']['sublevel'] ) ) {
251+
$id = (int) $_POST['sublevel']['toplevel']; // Bad x 1 - validate - key order is wrong.
252+
}
253+
}
254+
255+
function test_invalid_array_key_exists_call() {
256+
if ( array_key_exists( 'bar' ) ) {
257+
$id = (int) $_POST['bar']; // Bad x 1 - validate.
258+
}
259+
}

WordPress/Tests/Security/ValidatedSanitizedInputUnitTest.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,11 @@ public function getErrorList() {
6060
210 => 1,
6161
216 => 1,
6262
217 => 1,
63+
238 => 1,
64+
242 => 1,
65+
245 => 1,
66+
251 => 1,
67+
257 => 1,
6368
);
6469
}
6570

0 commit comments

Comments
 (0)