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.
Hi team,
I ran into a
TaintedSqlfalse negative on a real WordPress plugin SQL injection case,CVE-2025-7670The 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 andwpdb::get_results()as an SQL sink, the same flow is reported asTaintedSql:https://psalm.dev/r/424c87f8fd
The reported trace is exactly the one you would expect:
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:
For this CVE, the fix is conceptually the standard safe boundary:
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, andwpdbquery methods such asget_results(),query(),get_row(), andget_var()as SQL sinks. It would also be useful ifwpdb->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.