Make WordPress Core

Opened 5 months ago

Last modified 2 months ago

#64507 reviewing defect (bug)

Fatal errors can ensue in WP_Query when requests are made with array query vars

Reported by: leedxw's profile leedxw Owned by:
Milestone: Future Release Priority: normal
Severity: normal Version:
Component: Query Keywords: has-patch has-unit-tests
Focuses: Cc:

Description

Investigating 5XX errors, I see numerous fatal errors triggered because user provided value author_name is not validated to be a string.

(HTTP-provided arguments are strings or arrays)

To reproduce:

curl -g "http://localhost/?author_name[x]=bob" 
[15-Jan-2026 10:51:57 UTC] PHP Fatal error:  Uncaught TypeError: str_contains(): Argument #1 ($haystack) must be of type string, array given in /var/www/html/wp-includes/class-wp-query.php:2422
Stack trace:
#0 /var/www/html/wp-includes/class-wp-query.php(2422): str_contains()
#1 /var/www/html/wp-includes/class-wp-query.php(3958): WP_Query->get_posts()
#2 /var/www/html/wp-includes/class-wp.php(704): WP_Query->query()
#3 /var/www/html/wp-includes/class-wp.php(824): WP->query_posts()
#4 /var/www/html/wp-includes/functions.php(1343): WP->main()
#5 /var/www/html/wp-blog-header.php(16): wp()
#6 /var/www/html/index.php(17): require('...')
#7 {main}
  thrown in /var/www/html/wp-includes/class-wp-query.php on line 2422

Attachments (1)

author_name_string.patch (942 bytes) - added by leedxw 5 months ago.
patch to check if author_name is a string

Download all attachments as: .zip

Change History (8)

@leedxw
5 months ago

patch to check if author_name is a string

#1 @sabernhardt
5 months ago

  • Component changed from General to Query

Similar tickets: #59373, #62627

#2 @westonruter
5 months ago

  • Milestone changed from Awaiting Review to 7.0
  • Owner set to westonruter
  • Status changed from new to reviewing

Also relates to #64238, as PHPStan could in the future identify problems like this.

@leedxw Are you aware of the code which is making these malformed requests?

#3 @leedxw
5 months ago

This came as a result of investigating fatal errors caused by a hack attempts on a website - not legitimate code.

(Some of the patterns indicate the use of the Acunetix testing tool.)

#4 @westonruter
5 months ago

  • Keywords has-patch added

#6 @westonruter
5 months ago

  • Keywords needs-patch added; has-patch removed
  • Milestone changed from 7.0 to Future Release
  • Owner westonruter deleted
  • Summary changed from Fatal error when author_name is not a string to Fatal errors can ensue in WP_Query when requests are made with array query vars

I opened a PR for the patch and I started iterating on it a bit more to account for other query vars which may erroneously be arrays.

I'm sure there are many more query vars that need to be accounted for to prevent possible fatal errors from bad requests. It seems strange to limit the scope to author_name only when the error could occur for others. So I'll leave this open for other to find more opportunities to harden WP_Query in how the query vars are passed.

This ticket was mentioned in PR #11466 on WordPress/wordpress-develop by @sanket.parmar.


2 months ago
#7

  • Keywords has-patch has-unit-tests added; needs-patch removed

## Summary

Fixes a class of fatal TypeError exceptions in WP_Query on PHP 8+ that occur when HTTP requests use PHP's bracket notation (e.g. ?author_name[x]=bob, ?feed[]=rss2) to pass array values for query vars that expect strings.

## Root Cause

PHP's bracket notation delivers arrays via $_GET. WP_Query::fill_query_vars() only sets defaults for missing keys — it does not coerce types. So an array from HTTP passes through as-is.

PHP 8 made str_contains(), str_replace(), and wp_basename() strict about argument types; passing an array where a string is expected now raises a TypeError fatal rather than the E_WARNING PHP 7 issued.

## Confirmed Crash Sites

Query var Fatal expression Location
author_name str_contains( $query_vars['author_name'], '/' ) get_posts() ~L2424
feed str_contains( $query_vars['feed'], 'comments-' ) get_posts() ~L1036
feed str_replace( 'comments-', '', $query_vars['feed'] ) get_posts() ~L1037
attachment wp_basename( $query_vars['attachment'] ) get_posts() ~L2224

## Fix

In WP_Query::parse_query(), normalise author_name, feed, and attachment with an is_string() guard before they reach the crash sites:

$query_vars['author_name'] = is_string( $query_vars['author_name'] ) ? $query_vars['author_name'] : '';
$query_vars['feed']        = is_string( $query_vars['feed'] ) ? $query_vars['feed'] : '';
$query_vars['attachment']  = is_string( $query_vars['attachment'] ) ? $query_vars['attachment'] : '';

This follows the same established pattern already used for numeric vars (p, paged, year, etc.) in the same normalization block.

## Why Only These Three Vars — Not Others

Every other default string query var was considered and ruled out for one of three reasons:

Var(s) Reason no guard needed
cat, tag Arrays handled intentionally downstream — get_posts() explicitly checks if ( is_array( $query_vars['cat/tag'] ) ) { implode(...) } before any string function touches them
category_name Never passed to str_contains, str_replace, or wp_basename; only used in '' !== comparisons and get_term_by()
sentence, embed, preview, tb Boolean flags — only ever checked with ! empty() or '' !=; an is_string() guard would silently coerce true to '', breaking their intended boolean usage
fields Only used in switch / === string comparisons — no PHP 8 type-strict function call
meta_key, meta_value Documented as string\||string[]; WP_Meta_Query handles the array form; guarding would silently break new WP_Query(['meta_key' => ['k1', 'k2']])

## Tests

Two data-driven test methods added to Tests_Query_ParseQuery (ticket #64507), covering all three guarded vars:

  • test_parse_query_string_var_string_value — regression guard ensuring valid string values pass through unchanged.
  • test_parse_query_string_var_array_value — confirms array input is coerced to '' without fataling.

---

Trac ticket: https://core.trac.wordpress.org/ticket/64507

---

## Use of AI Tools

AI assistance: Yes
Tool(s): GitHub Copilot
Model(s): Claude Sonnet 4.6
Used for: Root cause analysis, crash site identification, initial patch and test implementation; final implementation, type contract verification, and correctness of guards reviewed and edited by me.

Note: See TracTickets for help on using tickets.