true, '$_FILES' => true, '$_GET' => false, '$_REQUEST' => false, ); /** * Custom list of functions which verify nonces. * * @since 0.5.0 * * @var string[] */ public $customNonceVerificationFunctions = array(); /** * List of the functions which verify nonces. * * @since 0.5.0 * @since 0.11.0 Changed from public static to protected non-static. * @since 3.0.0 - Moved from the generic `Sniff` class to this class. * - Visibility changed from `protected` to `private. * * @var array */ private $nonceVerificationFunctions = array( 'wp_verify_nonce' => true, 'check_admin_referer' => true, 'check_ajax_referer' => true, ); /** * Cache of previously added custom functions. * * Prevents having to do the same merges over and over again. * * @since 0.5.0 * @since 0.11.0 - Changed from public static to protected non-static. * - Changed the format from simple bool to array. * @since 3.0.0 - Property rename from `$addedCustomFunctions` to `$addedCustomNonceFunctions`. * - Visibility changed from `protected` to `private. * - Format changed from a multi-dimensional array to a single-dimensional array. * * @var array */ private $addedCustomNonceFunctions = array(); /** * Information on the all scopes that were checked to find a nonce verification in a particular file. * * The array will be in the following format: * ``` * array( * 'file' => (string) The name of the file. * 'cache' => (array) array( * # => array( The key is the token pointer to the "start" position. * 'end' => (int) The token pointer to the "end" position. * 'nonce' => (int|bool) The token pointer where n nonce check * was found, or false if none was found. * ) * ) * ) * ``` * * @since 3.0.0 * * @var array */ private $cached_results; /** * Returns an array of tokens this test wants to listen for. * * @return array */ public function register() { $targets = array( \T_VARIABLE => \T_VARIABLE ); $targets += Collections::listOpenTokensBC(); // We need to skip over lists. return $targets; } /** * Processes this test, when one of its tokens is encountered. * * @param int $stackPtr The position of the current token in the stack. * * @return int|void Integer stack pointer to skip forward or void to continue * normal file processing. */ public function process_token( $stackPtr ) { // Skip over lists as whatever is in those will always be assignments. if ( isset( Collections::listOpenTokensBC()[ $this->tokens[ $stackPtr ]['code'] ] ) ) { $open_close = Lists::getOpenClose( $this->phpcsFile, $stackPtr ); $skip_to = $stackPtr; if ( false !== $open_close ) { $skip_to = $open_close['closer']; } return $skip_to; } if ( ! isset( $this->superglobals[ $this->tokens[ $stackPtr ]['content'] ] ) ) { return; } if ( Scopes::isOOProperty( $this->phpcsFile, $stackPtr ) ) { // Property with the same name as a superglobal. Not our target. return; } // Determine the cache keys for this item. $cache_keys = array( 'file' => $this->phpcsFile->getFilename(), 'start' => 0, 'end' => $stackPtr, ); // If we're in a function, only look inside of it. // This doesn't take arrow functions into account as those are "open". $functionPtr = Conditions::getLastCondition( $this->phpcsFile, $stackPtr, array( \T_FUNCTION, \T_CLOSURE ) ); if ( false !== $functionPtr ) { $cache_keys['start'] = $this->tokens[ $functionPtr ]['scope_opener']; } $this->mergeFunctionLists(); $needs_nonce = $this->needs_nonce_check( $stackPtr, $cache_keys ); if ( false === $needs_nonce ) { return; } if ( $this->has_nonce_check( $stackPtr, $cache_keys, ( 'after' === $needs_nonce ) ) ) { return; } // If we're still here, no nonce-verification function was found. $error_code = 'Missing'; if ( false === $this->superglobals[ $this->tokens[ $stackPtr ]['content'] ] ) { $error_code = 'Recommended'; } MessageHelper::addMessage( $this->phpcsFile, 'Processing form data without nonce verification.', $stackPtr, $this->superglobals[ $this->tokens[ $stackPtr ]['content'] ], $error_code ); } /** * Determine whether or not a nonce check is needed for the current superglobal. * * @since 3.0.0 * * @param int $stackPtr The position of the current token in the stack of tokens. * @param array $cache_keys The keys for the applicable cache (to potentially set). * * @return string|false String "before" or "after" if a nonce check is needed. * FALSE when no nonce check is needed. */ protected function needs_nonce_check( $stackPtr, array $cache_keys ) { $in_nonce_check = ContextHelper::is_in_function_call( $this->phpcsFile, $stackPtr, $this->nonceVerificationFunctions ); if ( false !== $in_nonce_check ) { // This *is* the nonce check, so bow out, but do store to cache. // @todo Change to use arg unpacking once PHP < 5.6 has been dropped. $this->set_cache( $cache_keys['file'], $cache_keys['start'], $cache_keys['end'], $in_nonce_check ); return false; } if ( Context::inUnset( $this->phpcsFile, $stackPtr ) ) { // Variable is only being unset, no nonce check needed. return false; } if ( VariableHelper::is_assignment( $this->phpcsFile, $stackPtr, false ) ) { // Overwriting the value of a superglobal. return false; } $needs_nonce = 'before'; if ( ContextHelper::is_in_isset_or_empty( $this->phpcsFile, $stackPtr ) || ContextHelper::is_in_type_test( $this->phpcsFile, $stackPtr ) || VariableHelper::is_comparison( $this->phpcsFile, $stackPtr ) || VariableHelper::is_assignment( $this->phpcsFile, $stackPtr, true ) || ContextHelper::is_in_array_comparison( $this->phpcsFile, $stackPtr ) || ContextHelper::is_in_function_call( $this->phpcsFile, $stackPtr, UnslashingFunctionsHelper::get_functions() ) !== false || $this->is_only_sanitized( $this->phpcsFile, $stackPtr ) ) { $needs_nonce = 'after'; } return $needs_nonce; } /** * Check if this token has an associated nonce check. * * @since 0.5.0 * @since 3.0.0 - Moved from the generic `Sniff` class to this class. * - Visibility changed from `protected` to `private. * - New `$cache_keys` parameter. * - New `$allow_nonce_after` parameter. * * @param int $stackPtr The position of the current token in the stack of tokens. * @param array $cache_keys The keys for the applicable cache. * @param bool $allow_nonce_after Whether the nonce check _must_ be before the $stackPtr or * is allowed _after_ the $stackPtr. * * @return bool */ private function has_nonce_check( $stackPtr, array $cache_keys, $allow_nonce_after = false ) { $start = $cache_keys['start']; $end = $cache_keys['end']; // We allow for certain actions, such as an isset() check to come before the nonce check. // If this superglobal is inside such a check, look for the nonce after it as well, // all the way to the end of the scope. if ( true === $allow_nonce_after ) { $end = ( 0 === $start ) ? $this->phpcsFile->numTokens : $this->tokens[ $start ]['scope_closer']; } // Check against the cache. $current_cache = $this->get_cache( $cache_keys['file'], $start ); if ( false !== $current_cache['nonce'] ) { // If we have already found a nonce check in this scope, we just // need to check whether it comes before this token. It is OK if the // check is after the token though, if this was only an isset() check. return ( true === $allow_nonce_after || $current_cache['nonce'] < $stackPtr ); } elseif ( $end <= $current_cache['end'] ) { // If not, we can still go ahead and return false if we've already // checked to the end of the search area. return false; } $search_start = $start; if ( $current_cache['end'] > $start ) { // We haven't checked this far yet, but we can still save work by // skipping over the part we've already checked. $search_start = $this->cached_results['cache'][ $start ]['end']; } // Loop through the tokens looking for nonce verification functions. for ( $i = $search_start; $i < $end; $i++ ) { // Skip over nested closed scope constructs. if ( isset( Collections::closedScopes()[ $this->tokens[ $i ]['code'] ] ) || \T_FN === $this->tokens[ $i ]['code'] ) { if ( isset( $this->tokens[ $i ]['scope_closer'] ) ) { $i = $this->tokens[ $i ]['scope_closer']; } continue; } // If this isn't a function name, skip it. if ( \T_STRING !== $this->tokens[ $i ]['code'] ) { continue; } // If this is one of the nonce verification functions, we can bail out. if ( isset( $this->nonceVerificationFunctions[ $this->tokens[ $i ]['content'] ] ) ) { /* * Now, make sure it is a call to a global function. */ if ( ContextHelper::has_object_operator_before( $this->phpcsFile, $i ) === true ) { continue; } if ( ContextHelper::is_token_namespaced( $this->phpcsFile, $i ) === true ) { continue; } $this->set_cache( $cache_keys['file'], $start, $end, $i ); return true; } } // We're still here, so no luck. $this->set_cache( $cache_keys['file'], $start, $end, false ); return false; } /** * Helper function to retrieve results from the cache. * * @since 3.0.0 * * @param string $filename The name of the current file. * @param int $start The stack pointer searches started from. * * @return array */ private function get_cache( $filename, $start ) { if ( is_array( $this->cached_results ) && $filename === $this->cached_results['file'] && isset( $this->cached_results['cache'][ $start ] ) ) { return $this->cached_results['cache'][ $start ]; } return array( 'end' => 0, 'nonce' => false, ); } /** * Helper function to store results to the cache. * * @since 3.0.0 * * @param string $filename The name of the current file. * @param int $start The stack pointer searches started from. * @param int $end The stack pointer searched stopped at. * @param int|bool $nonce Stack pointer to the nonce verification function call or false if none was found. * * @return void */ private function set_cache( $filename, $start, $end, $nonce ) { if ( is_array( $this->cached_results ) === false || $filename !== $this->cached_results['file'] ) { $this->cached_results = array( 'file' => $filename, 'cache' => array( $start => array( 'end' => $end, 'nonce' => $nonce, ), ), ); return; } // Okay, so we know the current cache is for the current file. Check if we've seen this start pointer before. if ( isset( $this->cached_results['cache'][ $start ] ) === false ) { $this->cached_results['cache'][ $start ] = array( 'end' => $end, 'nonce' => $nonce, ); return; } // Update existing entry. if ( $end > $this->cached_results['cache'][ $start ]['end'] ) { $this->cached_results['cache'][ $start ]['end'] = $end; } $this->cached_results['cache'][ $start ]['nonce'] = $nonce; } /** * Merge custom functions provided via a custom ruleset with the defaults, if we haven't already. * * @since 0.11.0 Split out from the `process()` method. * * @return void */ protected function mergeFunctionLists() { if ( $this->customNonceVerificationFunctions !== $this->addedCustomNonceFunctions ) { $this->nonceVerificationFunctions = RulesetPropertyHelper::merge_custom_array( $this->customNonceVerificationFunctions, $this->nonceVerificationFunctions ); $this->addedCustomNonceFunctions = $this->customNonceVerificationFunctions; } } }