Make WordPress Core

Opened 9 months ago

Closed 3 months ago

Last modified 3 months ago

#63316 closed defect (bug) (fixed)

[E_WARNING] Undefined array key "host" in wp-includes/canonical.php on line 717

Reported by: artz91's profile ArtZ91 Owned by: johnjamesjacoby's profile johnjamesjacoby
Milestone: 6.9 Priority: normal
Severity: minor Version: 2.3
Component: Canonical Keywords: has-test-info has-patch dev-feedback has-unit-tests commit
Focuses: Cc:

Description

The error of level E_WARNING was catched by using set_error_handler.

The issue related to requests coming from the IP address 85.142.100.134 (which appears to be a scanner from cyberok.ru) that are triggering warnings in WordPress.

access log records:

domain:443 85.142.100.134 - - [19/Apr/2025:23:18:23 +0300] "GET / HTTP/1.1" 301 3405 "-" "Mozilla/5.0 (compatible; CyberOKInspect/1.0; +https://www.cyberok.ru/policy.html)"
domain:443 85.142.100.134 - - [19/Apr/2025:23:18:23 +0300] "GET / HTTP/1.1" 200 21814 "https://91.107.124.26/" "Mozilla/5.0 (compatible; CyberOKInspect/1.0; +https://www.cyberok.ru/policy.html)"
domain:443 85.142.100.134 - - [19/Apr/2025:23:19:41 +0300] "HELP" 400 3602 "-" "-"
domain:443 85.142.100.134 - - [19/Apr/2025:23:19:41 +0300] "EHLO" 400 3602 "-" "-"
domain:443 85.142.100.134 - - [19/Apr/2025:23:19:55 +0300] "GET / HTTP/1.0" 301 3410 "-" "-"
domain:443 85.142.100.134 - - [19/Apr/2025:23:19:41 +0300] "GET / HTTP/1.0" 301 3410 "-" "-"

Debug backtrace:

Array
(
    [0] => Array
        (
            [file] => \/var\/www\/domain\/wp-includes\/canonical.php
            [line] => 717
            [function] => {closure}
        )

    [1] => Array
        (
            [file] => \/var\/www\/domain\/wp-includes\/class-wp-hook.php
            [line] => 324
            [function] => redirect_canonical
        )

    [2] => Array
        (
            [file] => \/var\/www\/domain\/wp-includes\/class-wp-hook.php
            [line] => 348
            [function] => apply_filters
            [class] => WP_Hook
            [type] => ->
        )

    [3] => Array
        (
            [file] => \/var\/www\/domain\/wp-includes\/plugin.php
            [line] => 517
            [function] => do_action
            [class] => WP_Hook
            [type] => ->
        )

    [4] => Array
        (
            [file] => \/var\/www\/domain\/wp-includes\/template-loader.php
            [line] => 13
            [function] => do_action
        )

    [5] => Array
        (
            [file] => \/var\/www\/domain\/wp-blog-header.php
            [line] => 19
            [args] => Array
                (
                    [0] => \/var\/www\/domain\/wp-includes\/template-loader.php
                )

            [function] => require_once
        )

    [6] => Array
        (
            [file] => \/var\/www\/domain\/index.php
            [line] => 17
            [args] => Array
                (
                    [0] => \/var\/www\/domain\/wp-blog-header.php
                )

            [function] => require
        )

)

Attachments (1)

63316.2.diff (1.0 KB) - added by johnjamesjacoby 6 months ago.
Make sure both $original & $redirect have the expected array keys set, even if empty

Download all attachments as: .zip

Change History (34)

#1 @abcd95
9 months ago

  • Keywords needs-testing-info added

Hey @ArtZ91, Thanks for raising the ticket.

Could you possibly outline the details on how to reproduce this error?
A step-by-step guide or a screencast would be super helpful in identifying the root cause.

#2 @SirLouen
9 months ago

  • Keywords reporter-feedback added

@ArtZ91 I'm not 100% confident, but it appears that the scanner is accessing your host without the Host header, which might be triggering that warning.

When you fill this kind of reports, ideally I would recommend you to download this plugin into your site
https://wordpress.org/plugins/test-reports/ and send the environment variables or attach the Site Health > Info report

Which provides some variables of what you are using, php versions, server info, etc...

#3 @ArtZ91
9 months ago

Environment

  • WordPress: 6.8
  • PHP: 8.3.10
  • Server: Apache/2.4.52 (Ubuntu)
  • Database: mysqli (Server: 8.0.41-0ubuntu0.22.04.1 / Client: mysqlnd 8.3.10)
  • Browser: Chrome 135.0.0.0
  • OS: Windows 10/11
  • Theme: private 1.0
  • MU Plugins:
    • aios-firewall-loader.php
  • Plugins:
    • Admin Columns 4.7.7
    • Advanced Custom Fields PRO 6.4.0.1
    • All-In-One Security (AIOS) 5.4.0
    • Cyr-To-Lat 6.3.0
    • Disable Comments 2.4.7
    • Redirection 5.5.2
    • Show Current Template 0.5.2
    • SVG Support 2.5.14
    • Test Reports 1.2.0
    • UpdraftPlus - Backup/Restore 1.25.5
    • WP Crontrol 1.18.0
    • WP Mail Logging 1.14.0
    • WP Mail SMTP 4.4.0
    • Yoast Duplicate Post 4.5
    • Yoast SEO 24.9
    • User Switching 1.9.2

Steps to Reproduce

Not reproduced yet / Steps unknown

Additional Notes

There is no clear understanding yet which process is causing the problem.

Internal wp-cron requests from 127.0.0.1 are executed suspiciously at the same time as this problem, but the logs only catch a GET request to the site root / from the scanner.

"ERROR_LEVEL":"E_DEPRECATED",
"HTTPS":"on",
"HTTP_HOST":"NULL",
"HTTP_REFERER":"NULL",
"HTTP_USER_AGENT":"NULL",
"IS_WP_CRON":false,
"REMOTE_ADDR":"85.142.100.140",
"REQUEST_METHOD":"GET",
"REQUEST_URI":"\/",
"SCRIPT_FILENAME":"\/var\/www\/<domain>\/index.php",
"SERVER_NAME":"<domain>"

Last edited 9 months ago by ArtZ91 (previous) (diff)

#4 @SirLouen
9 months ago

  • Keywords reporter-feedback removed

@ArtZ91 I need to check, but judging from your environment, my intuition says that the problem is with Apache2, it’s not sanitizing the Host header.

I'm going to see if I can setup an Apache host a try again (wordpress-develop env by default uses nginx reverse proxy with FPM-PHP hosts for WP). Still if this is the problem, host existance should be checked before using it in the informed variable, hence, there is a potential issue to be sorted, just we need some reproduction steps to consistently reproduce this issue until it gets sorted.

#5 @SirLouen
9 months ago

  • Keywords has-testing-info added; needs-testing-info removed

Bug Reproduction Report

Description

✅ This report validates that the issue can be reproduced.

Environment 1

  • WordPress: 6.9-alpha-60093-src
  • PHP: 8.2.28
  • Server: Apache/2.4.63 (Unix)
  • Database: mysqli (Server: 8.4.5 / Client: mysqlnd 8.2.28)
  • Browser: Chrome 135.0.0.0
  • OS: Windows 10/11
  • Theme: Minimal Child Theme 1.0.2
  • MU Plugins: None activated
  • Plugins:
    • Test Reports 1.2.0

Environment 2

  • WordPress: 6.8.1
  • PHP: 8.2.18
  • Server: Apache/2.4.59 (Win64) OpenSSL/3.1.5
  • Database: mysqli (Server: 11.8.1-MariaDB-log / Client: mysqlnd 8.2.18)
  • Browser: Chrome 137.0.0.0
  • OS: Windows 10/11
  • Theme: Twenty Twenty-Five 1.2
  • MU Plugins: None activated
  • Plugins:
    • Test Reports 1.2.0

Reproduction Steps

  1. First, you need a server that can accept requests without a Host. I tried to make Nginx do this, but could not find an easy solution. So the only alternative was to use Apache2 (this is why I asked here for the environment vars, I had a suspicion that A2 was the culprit.
  1. Second, we need to make sure that Apache2 has the guard down. We need to set HttpProtocolOptions Unsafe for the VirtualHost
  1. Third, we should issue a request with very low standards. Like this one:
    curl --http1.0 -H 'Host:' http://localhost:8889
    

Note that in my case I'm using the wordpress-develop build, with an slightly modified version to use Apache2 instead of Nginx

If you want to run my config in wordpress-develop you can get my patch with
npm run grunt patch:https://github.com/WordPress/wordpress-develop/pull/8722

If you want to run with other system, like LocalWP, FlyEnv, Laravel Herd check this

  1. Your PHP Error Logs must be activated, check the logs are being generated before starting (for example, create a file in the wordpress installation called error.php and add something like error_log("test");
  2. Your WP installation must be the only host because we are sending a Host without header. Otherwise make sure that your VirtualHost is first in priority (in the Virtualhost file, add something like 000-myhost.conf, Apache priority is based on the filenames)
  3. You should not run HTTPS. Disable HTTPS for the test

Actual Results

  1. ✅ Error condition occurs (reproduced) in both environments.

Here is the debug log that displays the same error that the reporter is reporting: https://gist.github.com/SirLouen/16ad44e99dbcf9ef7bd932663ba48e2f#file-debug-log

Additional Notes

  • Despite A2 being the culprit, I believe that this warning should be handled.
  • I'm not 100% confident that it feels that it's complex that this error happens in any modern well configured server, but as always, better safe than sorry.
Last edited 7 months ago by SirLouen (previous) (diff)

This ticket was mentioned in PR #8723 on WordPress/wordpress-develop by @SirLouen.


9 months ago
#6

  • Keywords has-patch added

#7 @SirLouen
9 months ago

  • Keywords needs-testing added

Testing instructions and patched provided. Reading for testing.

#8 @wordpressdotorg
8 months ago

  • Keywords has-test-info added; has-testing-info removed

This ticket was mentioned in Slack in #core-test by sirlouen. View the logs.


8 months ago

This ticket was mentioned in Slack in #core-test by sirlouen. View the logs.


8 months ago

#11 @SirLouen
7 months ago

More Testing info

It seems that some versions of Curl (over 8.10.x) add automatically a Host despite you set the header with Host: empty explicitely

For this reason an alternative is using another tool like netcat

Create a file called for example request.txt like:

GET / HTTP/1.0

Note the empty line by the end, this is important depending on the OS you are testing this.

And then call this like: nc localhost 8080 < request.txt (or ncat in Windows, you may need nmap tools)

The thing here is that HTTP 1.0 is the key: An Apache server, accepting 1.0 insecure requests, a gift of a server... is the method to trigger these canonical errors.

@ArtZ91 upgrade that prehistoric server!!!

#12 @mindctrl
7 months ago

I was able to reproduce the warnings and confirm that the patch fixes them. https://github.com/WordPress/wordpress-develop/pull/8723#pullrequestreview-2932045724

This ticket was mentioned in Slack in #core by sirlouen. View the logs.


7 months ago

This ticket was mentioned in Slack in #core by audrasjb. View the logs.


7 months ago

This ticket was mentioned in Slack in #core by sirlouen. View the logs.


7 months ago

#16 @SirLouen
7 months ago

  • Version changed from 6.8 to 2.3

#17 @SirLouen
7 months ago

  • Keywords needs-testing removed

#18 @SirLouen
7 months ago

  • Keywords dev-feedback added

6.9.0 Milestone proposed during today's <bug-scrub>
More information

cc @SergeyBiryukov

#19 @SirLouen
7 months ago

  • Severity changed from normal to minor

This is a low priority report but a straightforward easy fix.

This ticket was mentioned in Slack in #core by sirlouen. View the logs.


7 months ago

#22 @johnjamesjacoby
6 months ago

I have a few concerns about this patch:

1.

|| ! isset( $original['host'] )

I totally see how it makes sense to not attempt to redirect if $original['host'] is missing, but I found contrary evidence this use-case is/was intended to be supported via #6890.

If $original['host'] really is allowed to be empty, we'll need to make sure it's set like we do for path & query.

2.

|| ! isset( $original['path'] ) 

Will break for valid & well-formed URLs like: https://example.com?p=100

parse_url() will not set the path key, but query (p=100) is still deserving of canonical redirection.


What if we add...

	if ( ! isset( $redirect['host'] ) ) {
		$redirect['host'] = '';
	}

...below...

	// Notice fixing.

...and then let it do what it normally would to figure things out?

#23 @SirLouen
6 months ago

@johnjamesjacoby I pushed that change to the PR with all the confidence, and then tested with my test report instructions and still is throwing the Warning, that doesn't solve the issue.

@johnjamesjacoby
6 months ago

Make sure both $original & $redirect have the expected array keys set, even if empty

#24 follow-up: @johnjamesjacoby
6 months ago

  • Milestone changed from Awaiting Review to 6.9
  • Owner set to johnjamesjacoby
  • Status changed from new to assigned

@SirLouen I see what you mean. I've dug a bit deeper and I think the patch I just attached should fix all instances where array keys are assumed to exist when they may not. Would love your eyes on it!

#25 in reply to: ↑ 24 @SirLouen
6 months ago

Replying to johnjamesjacoby:

@SirLouen I see what you mean. I've dug a bit deeper and I think the patch I just attached should fix all instances where array keys are assumed to exist when they may not. Would love your eyes on it!

It seems it has progressed with that patch

[24-Jul-2025 12:23:07 UTC] PHP Warning:  Undefined array key "scheme" in /var/www/src/wp-includes/canonical.php on line 766
[24-Jul-2025 12:23:07 UTC] PHP Stack trace:
[24-Jul-2025 12:23:07 UTC] PHP   1. {main}() /var/www/src/index.php:0
[24-Jul-2025 12:23:07 UTC] PHP   2. require_once() /var/www/src/index.php:19
[24-Jul-2025 12:23:07 UTC] PHP   3. require() /var/www/src/_index.php:17
[24-Jul-2025 12:23:07 UTC] PHP   4. require_once() /var/www/src/wp-blog-header.php:19
[24-Jul-2025 12:23:07 UTC] PHP   5. do_action($hook_name = 'template_redirect') /var/www/src/wp-includes/template-loader.php:13
[24-Jul-2025 12:23:07 UTC] PHP   6. WP_Hook->do_action($args = [0 => '']) /var/www/src/wp-includes/plugin.php:517
[24-Jul-2025 12:23:07 UTC] PHP   7. WP_Hook->apply_filters($value = '', $args = [0 => '']) /var/www/src/wp-includes/class-wp-hook.php:348
[24-Jul-2025 12:23:07 UTC] PHP   8. redirect_canonical($requested_url = '', $do_redirect = *uninitialized*) /var/www/src/wp-includes/class-wp-hook.php:324

We can simply use the same strategy adding scheme

This is the current error, still scheme has to be handled.

But I wonder why you think that we should not short-circuit in the absence of the Host. Or I think we should handled those faulty requests with missing elements.

Last edited 6 months ago by SirLouen (previous) (diff)

#26 @SirLouen
6 months ago

  • Keywords needs-unit-tests added

Given that there are so many values now, I have implemented a little mod in my PR that is a little simpler than so many conditionals.

Maybe we should be adding tests, but assertCanonical forces example.com so its impossible to trigger empty host and scheme.

I've only added one Unit Test to check for the absence of path and query as in #63733 but handling Host and Scheme is a little bit more tricky (at least with that function that is only testing for canonical paths)

Last edited 6 months ago by SirLouen (previous) (diff)

#27 @SirLouen
6 months ago

I've been doing some debugging, and I've located the issue of why there is an empty host on the error, but there can actually show a port.

Here, with my curl --http1.0 -H 'Host:' http://localhost:8889

$redirect host localhost and port 8889, but $original is port 8889, but host is empty

This is the culprit: $redirect is getting $user_home.

Then ut comes to this line with an empty host then. Even if we remove the port, it will get redirected there with :/// requested_url (no scheme, no host, no port)

As I say, leaving the empty host dangling through the function is not doing any good. Also, we can try to handle that :/// (or ://:port/) somewhere in the middle of redirect_canonical.

I would simply short-circuit, as I said. An Empty host can't be done with HTTP1.1+ and supporting HTTP1.0 doesn't make much sense in 2025, but still a "catch-all" in user_home could also make sense, though.

But if we are willing to let it go through, I'm still trying to figure out how to trigger this with a Unit Test.

Last edited 6 months ago by SirLouen (previous) (diff)

#28 @SirLouen
6 months ago

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

Found the workaround for unit testing these properties.
Kind of weird overall this behaviour, but this is what it takes.

#29 @mindctrl
4 months ago

  • Keywords needs-testing added

#30 @SirLouen
4 months ago

  • Keywords needs-testing removed

@mindctrl more than testing, what this need is Mr @johnjamesjacoby to provide some feedback to my last answer. Any testing attempts can be a waste if we don't reach a conclusion.

#31 @johnjamesjacoby
3 months ago

  • Keywords commit added

@SirLouen Thanks for your persistence here.

I'm comfortable committing your PR for 6.9.

It fixes the issue for me, too, and is the most minimally invasive way I can see to address this.

An Empty host can't be done with HTTP1.1+ and supporting HTTP1.0 doesn't make much sense in 2025, but still a "catch-all" in user_home could also make sense

Agree about actively supporting HTTP1.0. That's a larger decision that you can influence via this issue and help communicate more broadly in the future if/when the need arises.

#32 @johnjamesjacoby
3 months ago

  • Resolution set to fixed
  • Status changed from assigned to closed

In 61136:

Canonical: prevent "Undefined array key" PHP warnings when host is not set.

This change is necessary to prevent scanning tools from polluting debug/error logs of some hosting configurations with PHP warnings simply by omitting the Host header from their requests.

This commit makes sure that all of the required host, path, query, and scheme array keys inside of the redirect_canonical() function are always set after various operations have been performed on them.

It also includes 1 new test case and 2 additional tests, to verify the problem and its fix are working as intended, as well as a small modification to the get_canonical() phpunit helper specifically to account for HTTP_HOST maybe not being set.

Props artz91, johnjamesjacoby, mindctrl, sirlouen.

Fixes #63316.

#33 @johnjamesjacoby
3 months ago

Correction to the commit message above, there is 1 additional unit test and 1 data provider helper method.

Note: See TracTickets for help on using tickets.