Skip to content

Commit a598b08

Browse files
committed
ValidatedSanitizedInput/Sniff::is_validated(): allow for multi-level array key check
This commit builds onto earlier improvements to this sniff via PR 1634 and 1635 This implements the use of the new `get_array_access_keys()` method in the `WordPress.Security.ValidatedSanitizedInput` sniff and the `Sniff::is_validated()` method. This allow for more precise checking of whether the correct variable has been validated properly before use. This should prevent some false positives as well as false negatives. Includes unit tests.
1 parent 8664364 commit a598b08

File tree

4 files changed

+94
-19
lines changed

4 files changed

+94
-19
lines changed

WordPress/Sniff.php

Lines changed: 59 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1916,17 +1916,20 @@ protected function get_array_access_key( $stackPtr ) {
19161916
*
19171917
* @since 0.5.0
19181918
* @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.
19191921
*
1920-
* @param int $stackPtr The index of this token in the stack.
1921-
* @param string $array_key An array key to check for ("bar" in $foo['bar']).
1922-
* @param bool $in_condition_only Whether to require that this use of the
1923-
* variable occur within the scope of the
1924-
* validating condition, or just in the same
1925-
* 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).
19261929
*
19271930
* @return bool Whether the var is validated.
19281931
*/
1929-
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 ) {
19301933

19311934
if ( $in_condition_only ) {
19321935
/*
@@ -1977,11 +1980,14 @@ protected function is_validated( $stackPtr, $array_key = null, $in_condition_onl
19771980
}
19781981

19791982
$scope_end = $stackPtr;
1983+
}
19801984

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

1983-
$bare_array_key = $this->strip_quotes( $array_key );
1984-
$targets = array(
1989+
$bare_array_keys = array_map( array( $this, 'strip_quotes' ), $array_keys );
1990+
$targets = array(
19851991
\T_ISSET => 'construct',
19861992
\T_EMPTY => 'construct',
19871993
\T_UNSET => 'construct',
@@ -2016,11 +2022,15 @@ protected function is_validated( $stackPtr, $array_key = null, $in_condition_onl
20162022
continue;
20172023
}
20182024

2019-
// If we're checking for a specific array key (ex: 'hello' in
2025+
// If we're checking for specific array keys (ex: 'hello' in
20202026
// $_POST['hello']), that must match too. Quote-style, however, doesn't matter.
2021-
if ( isset( $array_key )
2022-
&& $this->strip_quotes( $this->get_array_access_key( $i ) ) !== $bare_array_key ) {
2023-
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+
}
20242034
}
20252035

20262036
return true;
@@ -2053,12 +2063,45 @@ protected function is_validated( $stackPtr, $array_key = null, $in_condition_onl
20532063
}
20542064

20552065
$params = $this->get_function_call_parameters( $i );
2056-
if ( $params[2]['raw'] !== $this->tokens[ $stackPtr ]['content'] ) {
2066+
if ( count( $params ) < 2 ) {
20572067
continue 2;
20582068
}
20592069

2060-
if ( isset( $array_key )
2061-
&& $this->strip_quotes( $params[1]['raw'] ) !== $bare_array_key ) {
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+
) {
2075+
continue 2;
2076+
}
2077+
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.
20622105
continue 2;
20632106
}
20642107

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)