Summary
ContextHelper::is_safe_casted() only inspects the token immediately preceding the superglobal. The common idiom
$id = (int) ( $_GET['id'] ?? 0 );
puts a ( between the cast and the superglobal, so the helper returns false and WordPress.Security.ValidatedSanitizedInput fires two errors — InputNotSanitized + MissingUnslash — even though the value is:
- validated (the
?? default provides a value when the key is unset),
- unslashed (slashes don't survive an
(int) / (bool) / (float) cast), and
- sanitised (the only output is a PHP scalar of the cast type).
Rewriting to (int) absint( wp_unslash( $_GET['id'] ?? 0 ) ) adds no safety.
Reproducer
<?php
$a = (int) $_GET['foo']; // OK today — direct cast adjacency.
$b = (int) ( $_GET['foo'] ?? 0 ); // Reported as 2 errors. Should be OK.
$c = (bool) ( $_POST['flag'] ?? false ); // Reported as 2 errors. Should be OK.
$d = (float) ( $_POST['rate'] ?? 0.0 ); // Reported as 2 errors. Should be OK.
Run with --sniffs=WordPress.Security.ValidatedSanitizedInput. Lines 3–5 each produce:
InputNotSanitized — Detected usage of a non-sanitized input variable: $_GET[…]
MissingUnslash — $_GET[…] not unslashed before sanitization. Use wp_unslash() or similar
Relation to #2210
Related but a different sanitiser mechanism. #2210 is about implicit numeric coercion — e.g. microtime( true ) - $_SERVER['REQUEST_TIME_FLOAT'] — where the arithmetic operator itself coerces the operand to a number. This ticket is about explicit type casts separated from the superglobal by grouping parentheses. They need different detection paths; I hit this one when submitting a plugin to .org and originally commented on #2210, then realised it's a distinct case and split it out.
Proposed fix
Walk outward through grouping parentheses when searching for a preceding cast. Stop at the first non-paren, non-cast token. This handles the three canonical cast/?? shapes above plus nested grouping ((int) ( ( $_POST['n'] ?? 0 ) )), and leaves function-call sites (whose preceding-token-before-paren is a T_STRING, not a cast) unaffected.
Cases deliberately out of scope for the first patch — ternary between the cast and the superglobal ((int) ( isset($_POST['n']) ? $_POST['n'] : 0 )), arithmetic operators in between ((int) ( strlen($_POST['x']) + 1 )) — need a walk via the superglobal's nested_parenthesis stack and are a natural follow-up.
PR
I have a patch + tests up at #2722.
Issue drafted with AI assistance (Claude).
Summary
ContextHelper::is_safe_casted()only inspects the token immediately preceding the superglobal. The common idiomputs a
(between the cast and the superglobal, so the helper returnsfalseandWordPress.Security.ValidatedSanitizedInputfires two errors —InputNotSanitized+MissingUnslash— even though the value is:??default provides a value when the key is unset),(int)/(bool)/(float)cast), andRewriting to
(int) absint( wp_unslash( $_GET['id'] ?? 0 ) )adds no safety.Reproducer
Run with
--sniffs=WordPress.Security.ValidatedSanitizedInput. Lines 3–5 each produce:Relation to #2210
Related but a different sanitiser mechanism. #2210 is about implicit numeric coercion — e.g.
microtime( true ) - $_SERVER['REQUEST_TIME_FLOAT']— where the arithmetic operator itself coerces the operand to a number. This ticket is about explicit type casts separated from the superglobal by grouping parentheses. They need different detection paths; I hit this one when submitting a plugin to .org and originally commented on #2210, then realised it's a distinct case and split it out.Proposed fix
Walk outward through grouping parentheses when searching for a preceding cast. Stop at the first non-paren, non-cast token. This handles the three canonical cast/
??shapes above plus nested grouping ((int) ( ( $_POST['n'] ?? 0 ) )), and leaves function-call sites (whose preceding-token-before-paren is aT_STRING, not a cast) unaffected.Cases deliberately out of scope for the first patch — ternary between the cast and the superglobal (
(int) ( isset($_POST['n']) ? $_POST['n'] : 0 )), arithmetic operators in between ((int) ( strlen($_POST['x']) + 1 )) — need a walk via the superglobal'snested_parenthesisstack and are a natural follow-up.PR
I have a patch + tests up at #2722.
Issue drafted with AI assistance (Claude).