close
Skip to content

InputNotSanitized / MissingUnslash: (int) ( $_GET['x'] ?? 0 ) not recognised as cast-sanitised #2723

@dd32

Description

@dd32

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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions