*/ private $slashed_superglobals = array( '$_COOKIE' => true, '$_GET' => true, '$_POST' => true, '$_REQUEST' => true, '$_SERVER' => true, ); /** * Returns an array of tokens this test wants to listen for. * * @return array */ public function register() { return array( \T_VARIABLE, \T_DOUBLE_QUOTED_STRING, \T_HEREDOC, ); } /** * Processes this test, when one of its tokens is encountered. * * @param int $stackPtr The position of the current token in the stack. * * @return void */ public function process_token( $stackPtr ) { // Handling string interpolation. if ( \T_DOUBLE_QUOTED_STRING === $this->tokens[ $stackPtr ]['code'] || \T_HEREDOC === $this->tokens[ $stackPtr ]['code'] ) { // Retrieve all embeds, but use only the initial variable name part. $interpolated_variables = array_map( static function ( $embed ) { return preg_replace( '`^(\{?\$\{?\(?)([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)(.*)$`', '$2', $embed ); }, TextStrings::getEmbeds( $this->tokens[ $stackPtr ]['content'] ) ); // Filter the embeds down to superglobals only. $interpolated_superglobals = array_filter( $interpolated_variables, static function ( $var_name ) { return ( 'GLOBALS' !== $var_name && Variables::isSuperglobalName( $var_name ) ); } ); foreach ( $interpolated_superglobals as $bad_variable ) { $this->phpcsFile->addError( 'Detected usage of a non-sanitized, non-validated input variable %s: %s', $stackPtr, 'InputNotValidatedNotSanitized', array( $bad_variable, $this->tokens[ $stackPtr ]['content'] ) ); } return; } /* Handle variables */ // Check if this is a superglobal we want to examine. if ( '$GLOBALS' === $this->tokens[ $stackPtr ]['content'] || Variables::isSuperglobalName( $this->tokens[ $stackPtr ]['content'] ) === false ) { return; } // If the variable is being unset, we don't care about it. if ( Context::inUnset( $this->phpcsFile, $stackPtr ) ) { return; } // If we're overriding a superglobal with an assignment, no need to test. if ( VariableHelper::is_assignment( $this->phpcsFile, $stackPtr ) ) { return; } // This superglobal is being validated. if ( ContextHelper::is_in_isset_or_empty( $this->phpcsFile, $stackPtr ) ) { return; } $array_keys = VariableHelper::get_array_access_keys( $this->phpcsFile, $stackPtr ); if ( empty( $array_keys ) ) { return; } $error_data = array( $this->tokens[ $stackPtr ]['content'] . '[' . implode( '][', $array_keys ) . ']' ); /* * Check for validation first. */ $validated = false; for ( $i = ( $stackPtr + 1 ); $i < $this->phpcsFile->numTokens; $i++ ) { if ( isset( Tokens::$emptyTokens[ $this->tokens[ $i ]['code'] ] ) ) { continue; } if ( \T_OPEN_SQUARE_BRACKET === $this->tokens[ $i ]['code'] && isset( $this->tokens[ $i ]['bracket_closer'] ) ) { // Skip over array keys. $i = $this->tokens[ $i ]['bracket_closer']; continue; } if ( \T_COALESCE === $this->tokens[ $i ]['code'] ) { $validated = true; } // Anything else means this is not a validation coalesce. break; } if ( false === $validated ) { $validated = ValidationHelper::is_validated( $this->phpcsFile, $stackPtr, $array_keys, $this->check_validation_in_scope_only ); } if ( false === $validated ) { $this->phpcsFile->addError( 'Detected usage of a possibly undefined superglobal array index: %s. Use isset() or empty() to check the index exists before using it', $stackPtr, 'InputNotValidated', $error_data ); } // If this variable is being tested with one of the `is_..()` functions, sanitization isn't needed. if ( ContextHelper::is_in_type_test( $this->phpcsFile, $stackPtr ) ) { return; } // If this is a comparison ('a' == $_POST['foo']), sanitization isn't needed. if ( VariableHelper::is_comparison( $this->phpcsFile, $stackPtr, false ) ) { return; } // If this is a comparison using the array comparison functions, sanitization isn't needed. if ( ContextHelper::is_in_array_comparison( $this->phpcsFile, $stackPtr ) ) { return; } // Now look for sanitizing functions. if ( ! $this->is_sanitized( $this->phpcsFile, $stackPtr, array( $this, 'add_unslash_error' ) ) ) { $this->phpcsFile->addError( 'Detected usage of a non-sanitized input variable: %s', $stackPtr, 'InputNotSanitized', $error_data ); } } /** * Add an error for missing use of unslashing. * * @since 0.5.0 * @since 3.0.0 - Moved from the `Sniff` class to this class. * - The `$phpcsFile` parameter was added. * * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. * @param int $stackPtr The index of the token in the stack * which is missing unslashing. * * @return void */ public function add_unslash_error( File $phpcsFile, $stackPtr ) { $tokens = $phpcsFile->getTokens(); $var_name = $tokens[ $stackPtr ]['content']; if ( isset( $this->slashed_superglobals[ $var_name ] ) === false ) { // WP doesn't slash these, so they don't need unslashing. return; } // We know there will be array keys as that's checked in the process_token() method. $array_keys = VariableHelper::get_array_access_keys( $phpcsFile, $stackPtr ); $error_data = array( $var_name . '[' . implode( '][', $array_keys ) . ']' ); $phpcsFile->addError( '%s not unslashed before sanitization. Use wp_unslash() or similar', $stackPtr, 'MissingUnslash', $error_data ); } }