Skip to content

Commit 7262444

Browse files
rodrigoprimojrfnl
authored andcommitted
WP/EnqueuedResourceParameters: remove eval() from is_falsy()
This PR is a fix for CVE-2026-45293 / [GHSA-3pwp-g2mj-5p3v](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.
1 parent 54719c0 commit 7262444

3 files changed

Lines changed: 122 additions & 102 deletions

File tree

‎WordPress/Sniffs/WP/EnqueuedResourceParametersSniff.php‎

Lines changed: 58 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,11 @@
1010
namespace WordPressCS\WordPress\Sniffs\WP;
1111

1212
use PHP_CodeSniffer\Util\Tokens;
13+
use PHPCSUtils\Tokens\Collections;
14+
use PHPCSUtils\Utils\Arrays;
1315
use PHPCSUtils\Utils\Numbers;
1416
use PHPCSUtils\Utils\PassedParameters;
17+
use PHPCSUtils\Utils\TextStrings;
1518
use WordPressCS\WordPress\AbstractFunctionParameterSniff;
1619

1720
/**
@@ -65,46 +68,17 @@ final class EnqueuedResourceParametersSniff extends AbstractFunctionParameterSni
6568
\T_NS_SEPARATOR => \T_NS_SEPARATOR, // Needed to handle fully qualified \false (PHPCS 3.x).
6669
);
6770

68-
/**
69-
* Token codes which are "safe" to accept to determine whether a version would evaluate to `false`.
70-
*
71-
* This array is enriched with several of the PHPCS token arrays in the register() method.
72-
*
73-
* @var array<int|string, int|string>
74-
*/
75-
private $safe_tokens = array(
76-
\T_NULL => \T_NULL,
77-
\T_FALSE => \T_FALSE,
78-
\T_TRUE => \T_TRUE,
79-
\T_LNUMBER => \T_LNUMBER,
80-
\T_DNUMBER => \T_DNUMBER,
81-
\T_CONSTANT_ENCAPSED_STRING => \T_CONSTANT_ENCAPSED_STRING,
82-
\T_START_NOWDOC => \T_START_NOWDOC,
83-
\T_NOWDOC => \T_NOWDOC,
84-
\T_END_NOWDOC => \T_END_NOWDOC,
85-
\T_OPEN_PARENTHESIS => \T_OPEN_PARENTHESIS,
86-
\T_CLOSE_PARENTHESIS => \T_CLOSE_PARENTHESIS,
87-
\T_STRING_CONCAT => \T_STRING_CONCAT,
88-
);
89-
9071
/**
9172
* Returns an array of tokens this test wants to listen for.
9273
*
9374
* Overloads and calls the parent method to allow for adding additional tokens to the
94-
* $false_tokens and $safe_tokens properties.
75+
* $false_tokens property.
9576
*
9677
* @return array
9778
*/
9879
public function register() {
9980
$this->false_tokens += Tokens::$emptyTokens;
10081

101-
$this->safe_tokens += Tokens::$emptyTokens;
102-
$this->safe_tokens += Tokens::$assignmentTokens;
103-
$this->safe_tokens += Tokens::$comparisonTokens;
104-
$this->safe_tokens += Tokens::$operators;
105-
$this->safe_tokens += Tokens::$booleanOperators;
106-
$this->safe_tokens += Tokens::$castTokens;
107-
10882
return parent::register();
10983
}
11084

@@ -194,6 +168,12 @@ public function process_parameters( $stackPtr, $group_name, $matched_content, $p
194168
/**
195169
* Determine if a range has a falsy value.
196170
*
171+
* Only a limited set of values is recognized as falsy:
172+
* - Boolean false.
173+
* - An integer or float equal to zero.
174+
* - A text string with the content `'0'` or `''` (single or double-quoted, heredoc, or nowdoc).
175+
* - An empty array.
176+
*
197177
* @param int $start The position to start looking from.
198178
* @param int $end The position to stop looking (inclusive).
199179
*
@@ -202,67 +182,76 @@ public function process_parameters( $stackPtr, $group_name, $matched_content, $p
202182
* couldn't be reliably determined.
203183
*/
204184
protected function is_falsy( $start, $end ) {
205-
206185
// Find anything excluding the false tokens.
207186
$has_non_false = $this->phpcsFile->findNext( $this->false_tokens, $start, ( $end + 1 ), true );
208187
// If no non-false tokens are found, we are good.
209188
if ( false === $has_non_false ) {
210189
return true;
211190
}
212191

213-
$code_string = '';
214-
for ( $i = $start; $i <= $end; $i++ ) {
215-
if ( isset( $this->safe_tokens[ $this->tokens[ $i ]['code'] ] ) === false ) {
216-
// Function call/variable or other token which makes it neigh impossible
217-
// to determine whether the actual value would evaluate to false.
192+
$target_ptr = $this->phpcsFile->findNext( Tokens::$emptyTokens, $start, ( $end + 1 ), true );
193+
194+
// An array only evaluates to false when it is empty.
195+
if ( isset( Collections::arrayOpenTokensBC()[ $this->tokens[ $target_ptr ]['code'] ] ) ) {
196+
$open_close = Arrays::getOpenClose( $this->phpcsFile, $target_ptr );
197+
if ( false === $open_close ) {
198+
// Short list assignment, not an array.
218199
return false;
219200
}
220201

221-
if ( isset( Tokens::$emptyTokens[ $this->tokens[ $i ]['code'] ] ) === true ) {
222-
continue;
223-
}
202+
// Bail if there is any non-empty token in the $ver parameter after the array, as that's a more complex
203+
// expression which can't be reliably evaluated.
204+
$next_after_array = $this->phpcsFile->findNext(
205+
Tokens::$emptyTokens,
206+
( $open_close['closer'] + 1 ),
207+
( $end + 1 ),
208+
true
209+
);
224210

225-
// Make sure that PHP 7.4 numeric literals and PHP 8.1 explicit octals don't cause problems.
226-
if ( \T_LNUMBER === $this->tokens[ $i ]['code'] || \T_DNUMBER === $this->tokens[ $i ]['code'] ) {
227-
$number_info = Numbers::getCompleteNumber( $this->phpcsFile, $i );
228-
$code_string .= $number_info['decimal'];
229-
$i = $number_info['last_token'];
230-
continue;
211+
if ( false !== $next_after_array ) {
212+
return false;
231213
}
232214

233-
// Make sure that when deprecated casts are used in the code under scan and the sniff is run on PHP 8.5,
234-
// the eval() won't cause a deprecation notice, borking the scan of the file.
235-
if ( \PHP_VERSION_ID >= 80500 ) {
236-
if ( \T_INT_CAST === $this->tokens[ $i ]['code'] ) {
237-
$code_string .= '(int)';
238-
continue;
239-
}
215+
$first_non_empty_in_array = $this->phpcsFile->findNext(
216+
Tokens::$emptyTokens,
217+
( $open_close['opener'] + 1 ),
218+
$open_close['closer'],
219+
true
220+
);
240221

241-
if ( \T_DOUBLE_CAST === $this->tokens[ $i ]['code'] ) {
242-
$code_string .= '(float)';
243-
continue;
244-
}
222+
return ( false === $first_non_empty_in_array );
223+
}
245224

246-
if ( \T_BOOL_CAST === $this->tokens[ $i ]['code'] ) {
247-
$code_string .= '(bool)';
248-
continue;
249-
}
225+
// Check if it is a '0' or '' string.
226+
if ( isset( Collections::textStringStartTokens()[ $this->tokens[ $target_ptr ]['code'] ] ) ) {
227+
if ( \T_DOUBLE_QUOTED_STRING === $this->tokens[ $target_ptr ]['code'] ) {
228+
// No need to examine as it will never match/can't be determined.
229+
return false;
230+
}
250231

251-
if ( \T_BINARY_CAST === $this->tokens[ $i ]['code'] ) {
252-
$code_string .= '(string)';
253-
continue;
254-
}
232+
$valid_tokens = array( \T_CONSTANT_ENCAPSED_STRING ) + Tokens::$heredocTokens + Tokens::$emptyTokens;
233+
if ( false !== $this->phpcsFile->findNext( $valid_tokens, $start, ( $end + 1 ), true ) ) {
234+
// Bail if the $ver parameter is more than a single text string.
235+
return false;
255236
}
256237

257-
$code_string .= $this->tokens[ $i ]['content'];
238+
$content = TextStrings::getCompleteTextString( $this->phpcsFile, $target_ptr );
239+
240+
return '0' === $content || '' === $content;
258241
}
259242

260-
if ( '' === $code_string ) {
243+
// The int/float check below only handles a single literal token, so bail if there is more than one non-empty token.
244+
if ( false !== $this->phpcsFile->findNext( Tokens::$emptyTokens, ( $target_ptr + 1 ), ( $end + 1 ), true ) ) {
261245
return false;
262246
}
263247

264-
// Evaluate the argument to figure out the outcome is false or not.
265-
// phpcs:ignore Squiz.PHP.Eval -- No harm here.
266-
return ( false === eval( "return (bool) $code_string;" ) );
248+
// Check if it is an int or float equal to zero.
249+
if ( \T_LNUMBER === $this->tokens[ $target_ptr ]['code'] || \T_DNUMBER === $this->tokens[ $target_ptr ]['code'] ) {
250+
$number_info = Numbers::getCompleteNumber( $this->phpcsFile, $target_ptr );
251+
252+
return 0.0 === (float) $number_info['decimal'];
253+
}
254+
255+
return false;
267256
}
268257
}

‎WordPress/Tests/WP/EnqueuedResourceParametersUnitTest.1.inc‎

Lines changed: 59 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,23 @@
33
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.
44

55
wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), /* comment */ '1.1.1', true ); // OK.
6-
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.
6+
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.
77
wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), '1.1.1', $in_footer ); // OK, the value is set.
88

9-
wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), 0, true ); // Error - 0, false or NULL are not allowed.
10-
wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), false, true ); // Error - 0, false or NULL are not allowed.
11-
wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), null, true ); // Error - 0, false or NULL are not allowed.
12-
wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), 0.0, true ); // Error - 0, false or NULL are not allowed.
13-
wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), 00.00, true ); // Error - 0, false or NULL are not allowed.
14-
wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), 0x0, true ); // Error - 0, false or NULL are not allowed.
9+
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.
10+
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.
11+
wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), null, true ); // Warning - $ver is NULL.
12+
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.
13+
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.
14+
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.
1515
wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), 100.001, true ); // OK.
1616
wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), 0x1, true ); // Hex number, OK.
1717
wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), 052, true ); // Octal number, OK.
1818
wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), 1, true ); // OK.
1919
wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), 0.1, true ); // OK.
2020
wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), 1.0, true ); // OK.
2121
wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), 2 * 8, true ); // OK.
22-
wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), '0', true ); // Error - 0, false or NULL are not allowed.
22+
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.
2323
wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), '0.0.0', true ); // OK.
2424
wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), '0.1.0', true ); // OK.
2525
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.
5151
wp_enqueue_style( 'someScript-js' ); // OK.
5252

5353
wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), (bool) 1, true ); // OK.
54-
wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), (bool) 0, true ); // Error - 0, false or NULL are not allowed.
54+
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.
5555

56-
wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), (binary) 123, true ); // OK.
57-
wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), b'0', true ); // Error - 0, false or NULL are not allowed.
56+
// Safeguard handling of arithmetic operations.
57+
wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), 0 + 1, true ); // OK.
5858

5959
// Safeguard support for PHP 8.0+ named parameters.
6060
wp_register_script(
61-
ver : 0, // Error - 0, false or NULL are not allowed.
61+
ver : 0, // Error - a falsy value is not allowed for the $ver parameter.
6262
in_footer: false,
6363
src : 'https://example.com/someScript.js',
6464
handle : 'someScript-js',
@@ -73,39 +73,39 @@ wp_register_script(
7373
'someScript-js',
7474
'https://example.com/someScript.js',
7575
array( 'jquery' ),
76-
// Error - 0, false or NULL are not allowed.
76+
// Warning - $ver is NULL.
7777
null,
7878
true,
7979
);
8080

8181
// Safeguard handling of PHP 7.4 numeric literals with underscores.
82-
wp_register_script( 'someScript-js', $url, [], 0_0.0_0, true ); // Error - 0, false or NULL are not allowed.
82+
wp_register_script( 'someScript-js', $url, [], 0_0.0_0, true ); // Error - a falsy value is not allowed for the $ver parameter.
8383

8484
// Safeguard handling of PHP 8.1 explicit octals.
85-
wp_register_script( 'someScript-js', $url, [], 0o0, true ); // Error - 0, false or NULL are not allowed.
85+
wp_register_script( 'someScript-js', $url, [], 0o0, true ); // Error - a falsy value is not allowed for the $ver parameter.
86+
87+
88+
89+
90+
91+
92+
93+
8694

87-
// Safeguard against PHP 8.5 deprecation of non-standard cast names.
88-
wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), (boolean) 1, true ); // OK.
89-
wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), (boolean) 0, true ); // Error - 0, false or NULL are not allowed.
9095

91-
wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), (integer) 1, true ); // OK.
92-
wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), (integer) 0, true ); // Error - 0, false or NULL are not allowed.
9396

94-
wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), (double) 1, true ); // OK.
95-
wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), (double) 0, true ); // Error - 0, false or NULL are not allowed.
9697

97-
wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), (binary) 0, true ); // Error - 0, false or NULL are not allowed.
9898

9999
// Safeguard handling of non-lowercase `null`.
100-
wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), NULL, true ); // Warning - 0, false or NULL are not allowed.
100+
wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), NULL, true ); // Warning - $ver is NULL.
101101

102102
/*
103103
* Safeguard handling of fully qualified \true, \false and \null.
104104
* Also safeguard that adding T_NS_SEPARATOR to $false_tokens doesn't cause false positives due to problems in is_falsy().
105105
*/
106-
wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), \FALSE, \true ); // Error - 0, false or NULL are not allowed.
107-
wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), \null, \TRUE ); // Warning - 0, false or NULL are not allowed.
108-
wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), \Null, true ); // Warning - 0, false or NULL are not allowed.
106+
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.
107+
wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), \null, \TRUE ); // Warning - $ver is NULL.
108+
wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), \Null, true ); // Warning - $ver is NULL.
109109
wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), \true, \False ); // Ok.
110110
wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), \get_version(), \null ); // OK.
111111

@@ -118,3 +118,35 @@ MyNamespace\wp_register_style( 'style-name', 'https://example.com/style.css' );
118118
\MyNamespace\wp_enqueue_style( 'style-name', 'https://example.com/style.css' ); // Ok.
119119
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").
120120
namespace\Sub\wp_enqueue_style( 'style-name', 'https://example.com/style.css' ); // Ok.
121+
122+
// Safeguard handling of an array passed as the $ver parameter. Only an empty array evaluates to false.
123+
wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), [ '1.0.0' ], true ); // OK.
124+
wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), [] + [ 1 ], true ); // OK - the array is part of a larger expression.
125+
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.
126+
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.
127+
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.
128+
129+
// Safeguard handling of a heredoc/nowdoc passed as the $ver parameter. Only an empty or "0" body evaluates to false.
130+
wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), <<<'EOD'
131+
1.0.0
132+
EOD
133+
, true ); // OK.
134+
wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), <<<EOD
135+
$version
136+
EOD
137+
, true ); // OK - contains interpolation, so the value can't be determined.
138+
wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), <<<"EOD"
139+
EOD
140+
. '.0', true ); // OK - the heredoc is part of a larger expression.
141+
wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), <<<'EOD'
142+
EOD
143+
, true ); // Error - a falsy value is not allowed for the $ver parameter.
144+
wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), <<<EOD
145+
0
146+
EOD
147+
, true ); // Error - a falsy value is not allowed for the $ver parameter.
148+
149+
// Safeguard handling of a double-quoted string passed as the $ver parameter.
150+
wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), "", true ); // Error - a falsy value is not allowed for the $ver parameter.
151+
wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), "$ver", true ); // Ok.
152+
wp_register_script( 'someScript-js', 'https://example.com/someScript.js' , array( 'jquery' ), "${0}", true ); // Ok.

‎WordPress/Tests/WP/EnqueuedResourceParametersUnitTest.php‎

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,16 +38,15 @@ public function getErrorList( $testFile = '' ) {
3838
13 => 1,
3939
14 => 1,
4040
22 => 1,
41-
54 => 1,
42-
57 => 1,
4341
61 => 1,
4442
82 => 1,
4543
85 => 1,
46-
89 => 1,
47-
92 => 1,
48-
95 => 1,
49-
97 => 1,
5044
106 => 1,
45+
126 => 1,
46+
127 => 1,
47+
141 => 1,
48+
144 => 1,
49+
150 => 1,
5150
);
5251

5352
case 'EnqueuedResourceParametersUnitTest.2.inc':

0 commit comments

Comments
 (0)