Skip to content

allow_network_redirect_hosts adds port-suffixed entries that fail wp_validate_redirect strict comparison, breaking cross-domain SSO on non-standard ports #1091

Description

@superdav42

Summary

WP_Ultimo\Domain_Mapping::allow_network_redirect_hosts() adds port-suffixed
entries (e.g. main.test:8080) to the allowed_redirect_hosts list, but
WordPress core's wp_validate_redirect() strips the port from the redirect
target's host before comparison. Strict equality fails, wp_safe_redirect()
falls back to the safe-default URL, and cross-domain SSO redirects from a
mapped subsite to the main site silently fail
when either domain runs on a
non-standard port.

Reproduction

  1. Multisite install on a non-standard port (e.g. DOMAIN_CURRENT_SITE = 'main.test:8080').
  2. Subsite served on a different host (mapped, or a row in wp_blogs with a
    different domain), still on :8080.
  3. As an unauthenticated user, hit http://mapped.test:8080/wp-admin/.

Expected: SSO redirect chain bounces the browser to
http://main.test:8080/wp-login.php?sso=login&return_url=...&redirect_to=....

Actual: wp_safe_redirect() rejects the SSO URL because
wp_validate_redirect() compares the parsed host (main.test, no port) against
the allowed entry (main.test:8080, with port). The fallback URL is used and
the browser ends up redirecting to its own subsite admin, never reaching the
main-site login.

Evidence

wp_safe_redirect() calls wp_validate_redirect( $location, $default ). Inside
that function (wp-includes/pluggable.php), the host is extracted with
wp_parse_url( $location, PHP_URL_HOST ) — port stripped. The comparison is
strict:

if ( in_array( $host, (array) $allowed_hosts, true ) ) {
    return $location;
}

allow_network_redirect_hosts() (inc/class-domain-mapping.php ~line 222)
adds the request $host as-is, including any :port suffix carried from
HTTP_HOST. WordPress core never strips ports for that filter, so the
allow-list ends up containing main.test:8080 and the comparison fails for
main.test.

Trace from a live test (debug mu-plugin, real domains redacted):

[debug-redirects] FALLBACK fired host=mapped.test:8080 uri=/wp-admin/
                  fallback=http://mapped.test:8080/wp-admin/
[debug-redirects] allowed_redirect_hosts called host=main.test
                  allowed=["main.test:8080"]
[debug-redirects] HOST=mapped.test:8080 URI=/wp-admin/
                  -> http://mapped.test:8080/wp-admin/ (status=302)

Suggested fix

allow_network_redirect_hosts() should add both the port-suffixed and
port-stripped versions of any host it allows. Sketch:

public function allow_network_redirect_hosts($allowed_hosts, $host) {
    if (empty($host)) {
        return $allowed_hosts;
    }

    $host         = strtolower($host);
    $host_no_port = preg_replace('/:\d+$/', '', $host);
    // ...existing lookup logic, but on every successful match push BOTH:
    //   $allowed_hosts[] = $host;          // "main.test:8080"
    //   $allowed_hosts[] = $host_no_port;  // "main.test"
}

Alternatively, when registering the filter, also normalise existing entries:
walk the allowed list once and append a port-stripped version of any
port-suffixed host.

Workaround

A small mu-plugin that walks allowed_redirect_hosts and adds port-stripped
versions of any port-suffixed entry is sufficient for dev environments:

add_filter('allowed_redirect_hosts', function ($hosts) {
    foreach ((array) $hosts as $h) {
        if (strpos($h, ':') !== false) {
            $stripped = explode(':', $h, 2)[0];
            if ($stripped && ! in_array($stripped, $hosts, true)) {
                $hosts[] = $stripped;
            }
        }
    }
    return array_values(array_unique($hosts));
}, 99);

Without this, every cross-domain SSO redirect from a mapped subsite on a
non-standard port silently falls back to the same-host fallback URL, defeating
the SSO flow.

Impact

Surfaces only when:

  • The network or mapped domain runs on a non-standard port, AND
  • Cross-domain SSO redirects are exercised (handle_auth_redirect,
    handle_login_redirect, handle_already_logged_in_on_login_page, or any
    other code path emitting a cross-host wp_safe_redirect).

Production installs on :80/:443 are unaffected because WordPress core
never carries those ports through to the host-comparison step. Most-impacted
users are local-dev setups testing the new cookie-less SSO flow added in #1086
and #1088.

Related


aidevops.sh v3.14.52 plugin for OpenCode v1.14.33 with claude-sonnet-4-6 spent 17h 17m on this as a headless worker.

Metadata

Metadata

Assignees

Labels

origin:workerAuto-created by pulse labelless backfill (t2112)status:in-reviewPR open, awaiting review/merge

Type

No type

Fields

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