Skip to content

TaintedSql false negative for WordPress request/wpdb flows #11825

@invoke1442

Description

@invoke1442

Hi team,
I ran into a TaintedSql false negative on a real WordPress plugin SQL injection case, CVE-2025-7670

The vulnerable pattern is straightforward: attacker-controlled data comes from WP_REST_Request::get_param(), is concatenated into an SQL string, and is then executed through $wpdb->get_results(). In the real plugin, the vulnerable route is unauthenticated and the fix replaces direct interpolation with $wpdb->prepare(...) plus stricter integer handling.

What seems to be missing here is not the taint engine itself, but default modeling for common WordPress APIs.

Using the official Psalm online validator, I reduced this to a minimal reproduction.

Without any WordPress-specific taint annotations, Psalm reports no issue:

https://psalm.dev/r/af730b594a

After adding only Psalm’s own taint annotations to model WP_REST_Request::get_param() as a taint source and wpdb::get_results() as an SQL sink, the same flow is reported as TaintedSql:

https://psalm.dev/r/424c87f8fd

The reported trace is exactly the one you would expect:

WP_REST_Request::get_param
$cats
SQL string concatenation
$sql
wpdb::get_results

That strongly suggests the false negative is caused by missing default modeling for WordPress request/database APIs, rather than by an inability to detect the SQL injection pattern itself.

In practice, this affects a common WordPress shape:

$cats = $request->get_param('cats');
$sql = "SELECT * FROM wp_posts WHERE term_id IN (" . $cats . ")";
return $wpdb->get_results($sql);

For this CVE, the fix is conceptually the standard safe boundary:

$sql = $wpdb->prepare(
    "SELECT * FROM wp_posts WHERE term_id IN (%s)",
    $cats
);
return $wpdb->get_results($sql);

So the useful improvement on Psalm’s side seems to be default taint modeling for WordPress, especially:

WP_REST_Request::get_param() as an input source, and wpdb query methods such as get_results(), query(), get_row(), and get_var() as SQL sinks. It would also be useful if wpdb->prepare() were treated as the relevant SQL escape / safe boundary.

I am not suggesting a plugin-specific rule for this one CVE. The issue looks broader than that: WordPress plugins commonly route user input through request objects and $wpdb, and right now that path appears to be invisible unless a project adds custom taint annotations.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions