From 726244460ac97dc9e75d1e32e37d5ad848af2f35 Mon Sep 17 00:00:00 2001 From: Rodrigo Primo Date: Wed, 24 Jun 2026 19:28:21 +0000 Subject: [PATCH] WP/EnqueuedResourceParameters: remove eval() from is_falsy() This PR is a fix for CVE-2026-45293 / [GHSA-3pwp-g2mj-5p3v](https://github.com/WordPress/WordPress-Coding-Standards/security/advisories/GHSA-3pwp-g2mj-5p3v). Replace the eval()-based logic with explicit token-level checks for a small set of literal falsy values: - Boolean false. - An integer or float equal to zero. - A text string with the content `'0'` or `''` (single or double-quoted, heredoc, or nowdoc). - An empty array. Other forms that the previous implementation recognised via eval() are no longer detected, such as a value wrapped in a type cast (e.g. `(int) 0`). Supporting them doesn't justify the risks of using eval(). This commit also clarifies test case comments as the previous version was inaccurate. "0, false or NULL" are not the only values that are not allowed. Also, NULL and missing $ver parameter generate a warning instead of an error. --- .../WP/EnqueuedResourceParametersSniff.php | 127 ++++++++---------- .../EnqueuedResourceParametersUnitTest.1.inc | 86 ++++++++---- .../WP/EnqueuedResourceParametersUnitTest.php | 11 +- 3 files changed, 122 insertions(+), 102 deletions(-) diff --git a/WordPress/Sniffs/WP/EnqueuedResourceParametersSniff.php b/WordPress/Sniffs/WP/EnqueuedResourceParametersSniff.php index 28b4ba8d6b..f821da1751 100644 --- a/WordPress/Sniffs/WP/EnqueuedResourceParametersSniff.php +++ b/WordPress/Sniffs/WP/EnqueuedResourceParametersSniff.php @@ -10,8 +10,11 @@ namespace WordPressCS\WordPress\Sniffs\WP; use PHP_CodeSniffer\Util\Tokens; +use PHPCSUtils\Tokens\Collections; +use PHPCSUtils\Utils\Arrays; use PHPCSUtils\Utils\Numbers; use PHPCSUtils\Utils\PassedParameters; +use PHPCSUtils\Utils\TextStrings; use WordPressCS\WordPress\AbstractFunctionParameterSniff; /** @@ -65,46 +68,17 @@ final class EnqueuedResourceParametersSniff extends AbstractFunctionParameterSni \T_NS_SEPARATOR => \T_NS_SEPARATOR, // Needed to handle fully qualified \false (PHPCS 3.x). ); - /** - * Token codes which are "safe" to accept to determine whether a version would evaluate to `false`. - * - * This array is enriched with several of the PHPCS token arrays in the register() method. - * - * @var array - */ - private $safe_tokens = array( - \T_NULL => \T_NULL, - \T_FALSE => \T_FALSE, - \T_TRUE => \T_TRUE, - \T_LNUMBER => \T_LNUMBER, - \T_DNUMBER => \T_DNUMBER, - \T_CONSTANT_ENCAPSED_STRING => \T_CONSTANT_ENCAPSED_STRING, - \T_START_NOWDOC => \T_START_NOWDOC, - \T_NOWDOC => \T_NOWDOC, - \T_END_NOWDOC => \T_END_NOWDOC, - \T_OPEN_PARENTHESIS => \T_OPEN_PARENTHESIS, - \T_CLOSE_PARENTHESIS => \T_CLOSE_PARENTHESIS, - \T_STRING_CONCAT => \T_STRING_CONCAT, - ); - /** * Returns an array of tokens this test wants to listen for. * * Overloads and calls the parent method to allow for adding additional tokens to the - * $false_tokens and $safe_tokens properties. + * $false_tokens property. * * @return array */ public function register() { $this->false_tokens += Tokens::$emptyTokens; - $this->safe_tokens += Tokens::$emptyTokens; - $this->safe_tokens += Tokens::$assignmentTokens; - $this->safe_tokens += Tokens::$comparisonTokens; - $this->safe_tokens += Tokens::$operators; - $this->safe_tokens += Tokens::$booleanOperators; - $this->safe_tokens += Tokens::$castTokens; - return parent::register(); } @@ -194,6 +168,12 @@ public function process_parameters( $stackPtr, $group_name, $matched_content, $p /** * Determine if a range has a falsy value. * + * Only a limited set of values is recognized as falsy: + * - Boolean false. + * - An integer or float equal to zero. + * - A text string with the content `'0'` or `''` (single or double-quoted, heredoc, or nowdoc). + * - An empty array. + * * @param int $start The position to start looking from. * @param int $end The position to stop looking (inclusive). * @@ -202,7 +182,6 @@ public function process_parameters( $stackPtr, $group_name, $matched_content, $p * couldn't be reliably determined. */ protected function is_falsy( $start, $end ) { - // Find anything excluding the false tokens. $has_non_false = $this->phpcsFile->findNext( $this->false_tokens, $start, ( $end + 1 ), true ); // If no non-false tokens are found, we are good. @@ -210,59 +189,69 @@ protected function is_falsy( $start, $end ) { return true; } - $code_string = ''; - for ( $i = $start; $i <= $end; $i++ ) { - if ( isset( $this->safe_tokens[ $this->tokens[ $i ]['code'] ] ) === false ) { - // Function call/variable or other token which makes it neigh impossible - // to determine whether the actual value would evaluate to false. + $target_ptr = $this->phpcsFile->findNext( Tokens::$emptyTokens, $start, ( $end + 1 ), true ); + + // An array only evaluates to false when it is empty. + if ( isset( Collections::arrayOpenTokensBC()[ $this->tokens[ $target_ptr ]['code'] ] ) ) { + $open_close = Arrays::getOpenClose( $this->phpcsFile, $target_ptr ); + if ( false === $open_close ) { + // Short list assignment, not an array. return false; } - if ( isset( Tokens::$emptyTokens[ $this->tokens[ $i ]['code'] ] ) === true ) { - continue; - } + // Bail if there is any non-empty token in the $ver parameter after the array, as that's a more complex + // expression which can't be reliably evaluated. + $next_after_array = $this->phpcsFile->findNext( + Tokens::$emptyTokens, + ( $open_close['closer'] + 1 ), + ( $end + 1 ), + true + ); - // Make sure that PHP 7.4 numeric literals and PHP 8.1 explicit octals don't cause problems. - if ( \T_LNUMBER === $this->tokens[ $i ]['code'] || \T_DNUMBER === $this->tokens[ $i ]['code'] ) { - $number_info = Numbers::getCompleteNumber( $this->phpcsFile, $i ); - $code_string .= $number_info['decimal']; - $i = $number_info['last_token']; - continue; + if ( false !== $next_after_array ) { + return false; } - // Make sure that when deprecated casts are used in the code under scan and the sniff is run on PHP 8.5, - // the eval() won't cause a deprecation notice, borking the scan of the file. - if ( \PHP_VERSION_ID >= 80500 ) { - if ( \T_INT_CAST === $this->tokens[ $i ]['code'] ) { - $code_string .= '(int)'; - continue; - } + $first_non_empty_in_array = $this->phpcsFile->findNext( + Tokens::$emptyTokens, + ( $open_close['opener'] + 1 ), + $open_close['closer'], + true + ); - if ( \T_DOUBLE_CAST === $this->tokens[ $i ]['code'] ) { - $code_string .= '(float)'; - continue; - } + return ( false === $first_non_empty_in_array ); + } - if ( \T_BOOL_CAST === $this->tokens[ $i ]['code'] ) { - $code_string .= '(bool)'; - continue; - } + // Check if it is a '0' or '' string. + if ( isset( Collections::textStringStartTokens()[ $this->tokens[ $target_ptr ]['code'] ] ) ) { + if ( \T_DOUBLE_QUOTED_STRING === $this->tokens[ $target_ptr ]['code'] ) { + // No need to examine as it will never match/can't be determined. + return false; + } - if ( \T_BINARY_CAST === $this->tokens[ $i ]['code'] ) { - $code_string .= '(string)'; - continue; - } + $valid_tokens = array( \T_CONSTANT_ENCAPSED_STRING ) + Tokens::$heredocTokens + Tokens::$emptyTokens; + if ( false !== $this->phpcsFile->findNext( $valid_tokens, $start, ( $end + 1 ), true ) ) { + // Bail if the $ver parameter is more than a single text string. + return false; } - $code_string .= $this->tokens[ $i ]['content']; + $content = TextStrings::getCompleteTextString( $this->phpcsFile, $target_ptr ); + + return '0' === $content || '' === $content; } - if ( '' === $code_string ) { + // The int/float check below only handles a single literal token, so bail if there is more than one non-empty token. + if ( false !== $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $target_ptr + 1 ), ( $end + 1 ), true ) ) { return false; } - // Evaluate the argument to figure out the outcome is false or not. - // phpcs:ignore Squiz.PHP.Eval -- No harm here. - return ( false === eval( "return (bool) $code_string;" ) ); + // Check if it is an int or float equal to zero. + if ( \T_LNUMBER === $this->tokens[ $target_ptr ]['code'] || \T_DNUMBER === $this->tokens[ $target_ptr ]['code'] ) { + $number_info = Numbers::getCompleteNumber( $this->phpcsFile, $target_ptr ); + + return 0.0 === (float) $number_info['decimal']; + } + + return false; } } diff --git a/WordPress/Tests/WP/EnqueuedResourceParametersUnitTest.1.inc b/WordPress/Tests/WP/EnqueuedResourceParametersUnitTest.1.inc index 8592a34121..f1bb016792 100644 --- a/WordPress/Tests/WP/EnqueuedResourceParametersUnitTest.1.inc +++ b/WordPress/Tests/WP/EnqueuedResourceParametersUnitTest.1.inc @@ -3,15 +3,15 @@ wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ) ); // Warning - missing $ver, Warning - In Footer is set to a falsy (default) value. wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), /* comment */ '1.1.1', true ); // OK. -wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), /* comment */ '0' /* another comment */, true ); // Error - 0, false or NULL are not allowed. +wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), /* comment */ '0' /* another comment */, true ); // Error - a falsy value is not allowed for the $ver parameter. wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), '1.1.1', $in_footer ); // OK, the value is set. -wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), 0, true ); // Error - 0, false or NULL are not allowed. -wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), false, true ); // Error - 0, false or NULL are not allowed. -wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), null, true ); // Error - 0, false or NULL are not allowed. -wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), 0.0, true ); // Error - 0, false or NULL are not allowed. -wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), 00.00, true ); // Error - 0, false or NULL are not allowed. -wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), 0x0, true ); // Error - 0, false or NULL are not allowed. +wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), 0, true ); // Error - a falsy value is not allowed for the $ver parameter. +wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), false, true ); // Error - a falsy value is not allowed for the $ver parameter. +wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), null, true ); // Warning - $ver is NULL. +wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), 0.0, true ); // Error - a falsy value is not allowed for the $ver parameter. +wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), 00.00, true ); // Error - a falsy value is not allowed for the $ver parameter. +wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), 0x0, true ); // Error - a falsy value is not allowed for the $ver parameter. wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), 100.001, true ); // OK. wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), 0x1, true ); // Hex number, OK. wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), 052, true ); // Octal number, OK. @@ -19,7 +19,7 @@ wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), 0.1, true ); // OK. wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), 1.0, true ); // OK. wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), 2 * 8, true ); // OK. -wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), '0', true ); // Error - 0, false or NULL are not allowed. +wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), '0', true ); // Error - a falsy value is not allowed for the $ver parameter. wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), '0.0.0', true ); // OK. wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), '0.1.0', true ); // OK. wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), '0' . '0', true ); // OK. @@ -51,14 +51,14 @@ wp_register_style( 'someScript-js' ); // OK. wp_enqueue_style( 'someScript-js' ); // OK. wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), (bool) 1, true ); // OK. -wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), (bool) 0, true ); // Error - 0, false or NULL are not allowed. +wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), (bool) 0, true ); // OK - a falsy value behind a cast is not flagged. -wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), (binary) 123, true ); // OK. -wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), b'0', true ); // Error - 0, false or NULL are not allowed. +// Safeguard handling of arithmetic operations. +wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), 0 + 1, true ); // OK. // Safeguard support for PHP 8.0+ named parameters. wp_register_script( - ver : 0, // Error - 0, false or NULL are not allowed. + ver : 0, // Error - a falsy value is not allowed for the $ver parameter. in_footer: false, src : 'https://example.com/someScript.js', handle : 'someScript-js', @@ -73,39 +73,39 @@ wp_register_script( 'someScript-js', 'https://example.com/someScript.js', array( 'jquery' ), - // Error - 0, false or NULL are not allowed. + // Warning - $ver is NULL. null, true, ); // Safeguard handling of PHP 7.4 numeric literals with underscores. -wp_register_script( 'someScript-js', $url, [], 0_0.0_0, true ); // Error - 0, false or NULL are not allowed. +wp_register_script( 'someScript-js', $url, [], 0_0.0_0, true ); // Error - a falsy value is not allowed for the $ver parameter. // Safeguard handling of PHP 8.1 explicit octals. -wp_register_script( 'someScript-js', $url, [], 0o0, true ); // Error - 0, false or NULL are not allowed. +wp_register_script( 'someScript-js', $url, [], 0o0, true ); // Error - a falsy value is not allowed for the $ver parameter. + + + + + + + + -// Safeguard against PHP 8.5 deprecation of non-standard cast names. -wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), (boolean) 1, true ); // OK. -wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), (boolean) 0, true ); // Error - 0, false or NULL are not allowed. -wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), (integer) 1, true ); // OK. -wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), (integer) 0, true ); // Error - 0, false or NULL are not allowed. -wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), (double) 1, true ); // OK. -wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), (double) 0, true ); // Error - 0, false or NULL are not allowed. -wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), (binary) 0, true ); // Error - 0, false or NULL are not allowed. // Safeguard handling of non-lowercase `null`. -wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), NULL, true ); // Warning - 0, false or NULL are not allowed. +wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), NULL, true ); // Warning - $ver is NULL. /* * Safeguard handling of fully qualified \true, \false and \null. * Also safeguard that adding T_NS_SEPARATOR to $false_tokens doesn't cause false positives due to problems in is_falsy(). */ -wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), \FALSE, \true ); // Error - 0, false or NULL are not allowed. -wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), \null, \TRUE ); // Warning - 0, false or NULL are not allowed. -wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), \Null, true ); // Warning - 0, false or NULL are not allowed. +wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), \FALSE, \true ); // Error - a falsy value is not allowed for the $ver parameter. +wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), \null, \TRUE ); // Warning - $ver is NULL. +wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), \Null, true ); // Warning - $ver is NULL. wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), \true, \False ); // Ok. wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), \get_version(), \null ); // OK. @@ -118,3 +118,35 @@ MyNamespace\wp_register_style( 'style-name', 'https://example.com/style.css' ); \MyNamespace\wp_enqueue_style( 'style-name', 'https://example.com/style.css' ); // Ok. namespace\wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), '1.1.0' ); // The sniff should start flagging this once it can resolve relative namespaces (once it does, it should be "Warning - missing $in_footer"). namespace\Sub\wp_enqueue_style( 'style-name', 'https://example.com/style.css' ); // Ok. + +// Safeguard handling of an array passed as the $ver parameter. Only an empty array evaluates to false. +wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), [ '1.0.0' ], true ); // OK. +wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), [] + [ 1 ], true ); // OK - the array is part of a larger expression. +wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), [ $a ] = array( 1, 2, 3 ), true ); // OK - short list assignment, not an array. +wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), array(), true ); // Error - a falsy value is not allowed for the $ver parameter. +wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), [ /* comment */ ], true ); // Error - a falsy value is not allowed for the $ver parameter. + +// Safeguard handling of a heredoc/nowdoc passed as the $ver parameter. Only an empty or "0" body evaluates to false. +wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), <<<'EOD' +1.0.0 +EOD +, true ); // OK. +wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), << 1, 14 => 1, 22 => 1, - 54 => 1, - 57 => 1, 61 => 1, 82 => 1, 85 => 1, - 89 => 1, - 92 => 1, - 95 => 1, - 97 => 1, 106 => 1, + 126 => 1, + 127 => 1, + 141 => 1, + 144 => 1, + 150 => 1, ); case 'EnqueuedResourceParametersUnitTest.2.inc':