-
1.0.3
StableAll checks were successfulCI / Lint + Static Analysis (push) Successful in 1m48sCI / Unit Tests (PHP 8.5) (push) Successful in 1m40sRelease / Verify tag, version and changelog (push) Successful in 3sRelease / Unit Tests (PHP 8.4) (push) Successful in 1m34sRelease / Unit Tests (PHP 8.5) (push) Successful in 2m6sCI / Unit Tests (PHP 8.3) (push) Successful in 1m41sCI / Unit Tests (PHP 8.4) (push) Successful in 1m41sRelease / Lint + Static Analysis (push) Successful in 1m51sRelease / Unit Tests (PHP 8.3) (push) Successful in 1m39sRelease / Build zip and publish release (push) Successful in 3sreleased this
2026-10-16 16:13:41 -07:00 | 1 commits to main since this releaseMultisite upgrade notice: every host list saved by 1.0.2 or earlier is ignored after upgrading, on every site including the main site, until a network administrator reviews and saves it on that site's settings page. Until then that site does not rewrite URLs, so onion and other alias visitors are served the site's stored (clearnet) URLs. Network Admin lists the affected sites (#25). Single-site installs are unaffected. See the #21 entry under Security.
Added
- Opt-in update check (#19). A new Check for new versions of this plugin setting (off by default, option
multi_domain_redux_update_check) answers core'supdate_plugins_git.pickysysadmin.cafilter, which the existingUpdate URIheader makes core call. It reads the highest release tag (X.Y.Z/X.Y.Z.W) from the repo'stags.rss, and newer versions appear on Plugins and Dashboard → Updates with one-click update from the release zip. Results are cached for 12 hours (1 hour after a failed fetch). The request uses aMulti-Domain-Redux/<version>user agent so core's default (which includes the site URL) doesn't disclose the site address to the forge. On multisite the main site's setting applies; sub-site settings pages show it read-only and don't save it (#22). Uninstall removes the option and the cache. A cached version that isn't a release-style tag (written by something other than the feed check) is discarded and refetched rather than used in the download URL (#23). A tag appears in the feed a few minutes before its release zip is attached, and an update attempted in that window fails with a download error.
Changed
- Minimum PHP version raised from 8.2 to 8.3 (
Requires PHP: 8.3). PHP 8.2 reaches end of life in December 2026. Sites still on PHP 8.2 should stay on 1.0.2 until they upgrade PHP. - CI and release testing dropped PHP 8.2; unit tests now run on PHP 8.3 / 8.4 / 8.5.
- Removed the unused
UrlRewriter::urlFor()and a duplicateget_avatar_urlfilter registration.
Security
- A
Hostheader with a trailing root dot (<alias>., the FQDN form of the same host) failed validation and fell back to the primary host. Behind a front cache keyed on the dot-normalized host (e.g. nginxfastcgi_cache_keyusing$host), one unauthenticated request could poison an alias's (including the onion's) cache entry with primary-host URLs for every later visitor.HostHeader::normalize()now strips exactly one trailing dot before validation, so<alias>.resolves to the configured alias;example.com..is still rejected. The same normalization applies when saving the host list in settings. - Multisite: a sub-site administrator could set a host list that rewrote network-scoped URLs (password-reset links, network admin, sign-up, other blogs' URLs) onto a host they control. The settings page and
options.phpsaves now requiremanage_network_optionson multisite (manage_optionson single-site is unchanged). As defence in depth, on multisitenetwork_home_url/network_site_urlare no longer filtered, and URLs for another blog (viablog_idor whileswitch_to_blog()is active) are left untouched. - Multisite: host lists saved before 1.0.3 are no longer applied (#21). The fix above only governs new saves; a list a sub-site administrator had already saved kept rewriting that site's lost-password link (which core builds from
network_site_url()and passes throughlostpassword_url) onto a host they chose, for visitors on an unlisted host such as the site's own subdomain, and kept that host inallowed_redirect_hosts. Stored settings are now version 2, written only by the settings page (which requiresmanage_network_optionson multisite). On multisite a list with an older version is ignored until a network administrator reviews and saves it; until then an admin notice tells them so, and the settings page still shows the stored list. Network Admin also shows a notice listing every site (with the plugin active) whose list awaits review, each linked to its settings page (#25); the scan is cached for an hour in themulti_domain_redux_review_sitessite transient, cleared when any site's list or plugin activation changes, and removed on uninstall. Lists written withwp option updateneed"version": 2to apply on multisite. Single-site is unaffected. As defence in depth, on a sub-site whose domain differs from the network's, URLs on the network domain are left unchanged whichever filter carries them. - WP-Cron starvation via the onion host. Core's
spawn_cron()builds its server-to-self loopback fromsite_url(), which the plugin rewrote to the visitor's host. A request arriving on the onion host took the 60-seconddoing_cronlock and then posted tohttp://<onion>/wp-cron.php, which the PHP host normally cannot resolve, so due jobs never ran and no other request could spawn cron until the lock expired. One unauthenticated request per minute (or ordinary onion traffic) could suppress scheduled posts, update checks and other cron work indefinitely. Acron_requestfilter now pins the loopback to the storedsiteurl(keeping thedoing_wp_cronquery); targets on a host outside the configured list are left alone. Sites whose server cannot reach even its ownsiteurlshould useDISABLE_WP_CRONwith a system cron. - Scheme downgrade of login, password-reset, admin and moderation URLs. The URL rewriter replaced the scheme of every rewritten URL with the current request's scheme, discarding an
httpsscheme core had chosen viaFORCE_SSL_ADMINor anhttpshome/siteurl. On plain-http requests and in WP-CLI or system-cron runs, password-reset emails,wp_login_url(),admin_url(), thewp_login_form()action and comment-moderation email links were emitted ashttp://, exposing reset keys or passwords to an on-path observer where HSTS is not in force.UrlRewriterandContentRewriternow never downgrade: anhttpsURL stayshttpson clearnet hosts whatever the request scheme; onion hosts are still alwayshttp. This also fixes a redirect loop on plain-httpwp-login.php/wp-adminunderFORCE_SSL_ADMIN: core's https-upgradeLocationwas rewritten back tohttp://. - Request host persisted into block navigation and post guids, then replayed on every host. URLs core builds during a request follow that request's host, and some of them are stored: block-theme navigation fallback (a classic menu converted to a
wp_navigationpost on the first front-end render, by any anonymous visitor) and a new post's guid (set fromget_permalink(), e.g. by an author openingpost-new.phpon the onion). Block output and guids were not rewritten on output, so one request could put onion links in the clearnet header navigation, and clearnet RSS<guid>/ RESTguid.renderedcould disclose the onion address. Block output (render_block,widget_block_content) is now rewritten to the visitor's host like post content, and inline content rewriting now also normalises the scheme of URLs already on the visitor's host (never downgradinghttpson clearnet; onion stayshttp). Guids on a non-primary configured host are mapped to the primary host on output (get_the_guid, which also repairs guids already stored) and pinned there when a post is inserted. Primary-host guids are emitted unchanged so feed readers don't see existing items as new. - Hardening: the redirect allowlist no longer relaxes core's SSRF guard. Core's
http_request_host_is_externalfilter consultsallowed_redirect_hosts(viawp_validate_redirect()), so adding every configured host there also letwp_safe_remote_*()(pingback source fetches, oEmbed discovery) reach any configured host that resolves to a private address, on ports 80/443/8080. While that filter runs, the plugin now neither adds its hosts nor rewriteshome_url(), so outbound requests trust only the stored home host, as without the plugin.wp_safe_redirect()to configured hosts is unchanged. - Hardening: under
MULTI_DOMAIN_REDUX_TRUST_PROXY, comma-separatedX-Forwarded-HostandX-Forwarded-Protonow use the rightmost trimmed entry (the value the nearest proxy wrote) instead of the first. Behind a proxy that appends rather than overwrites (e.g. Apachemod_proxy), the first entry is client-supplied, so the client could pick which configured host a page was rendered for.X-Forwarded-Proto: http, httpsor a paddedhttpswas also treated ashttp. A list inHost(or in a host saved on the settings page) is now rejected instead of being truncated to its first entry. The documented setup (proxy strips clientX-Forwarded-Host) was not affected. - Hardening: stored host settings are re-validated on load. The settings sanitizer only runs on saves through the settings page, so values written by
wp option update, direct database edits or other plugins reached the rewriters unchecked: an empty host row made inline content rewriting match every bare//(JS comments,http://), an empty primary emittedhttp:///pathredirects and added''toallowed_redirect_hosts, and a non-string host fatally broke every request.Settings::fromArray()now normalizes each hostname as the sanitizer does, drops rows that are empty, invalid, non-scalar or duplicated, blanks an invalid base path or locale, and enforces a single primary. As defence in depth, the URL and content rewriters never emit or match an empty host. - Hardening: inline content rewriting no longer matches a configured host that isn't at the start of a link. The pattern had no left boundary, so a
//<host>path segment (http://example.com//example.com/x,/foo//example.com) was rewritten into the host, producing a broken URL likehttps://www.example.comhttps://www.example.com/x, andftp:///wss://links to a configured host were mangled the same way. A match must now not follow a word character,.,:,/or-. Links after quotes,=,(, whitespace or entities are rewritten as before. - Hardening: fewer core emitters disclose the primary host on alias (e.g. onion) pages. The REST index
urlfield (the rawsiteurloption) andget_header_image()/get_background_image()(absolute URLs stored as theme mods) are now rewritten to the visitor's host viarest_index,theme_mod_header_imageandtheme_mod_background_image. The hreflangx-defaultlink is now emitted only when at least one host has a locale; previously a site with no localized hosts published the primary host in anx-defaultlink on every page of every alias. When hosts do have locales, the hreflang tags (includingx-default→ primary) still name those hosts on every alias, by design.
Fixed
- Onion hosts: login and wp-admin were unreachable under
FORCE_SSL_ADMIN. Core redirected plain-httpwp-login.php/wp-adminto https, which the rewriter (onion is alwayshttp) sent back tohttp://<onion>, looping, andauth_redirect()only accepted the secure auth cookie a plain-http login never sets. On a request for an onion host the plugin now callsforce_ssl_admin( false )and filterssecure_signon_cookietofalse, so the site can be administered over Tor, which already encrypts the connection end to end. Clearnet hosts are unchanged. - Uninstall on multisite now removes the plugin's option from every site, not only the first 100 (
get_sites()default limit). - Canonical redirects are judged against the effective primary host (
Settings::primary()) rather than theis_primaryflag, so a host list with no flagged primary no longer suppressesredirect_canonicaleverywhere.
Documentation
- Configured hosts are assumed to be on their scheme's default port. Hosts have no port setting, and rewritten URLs drop any explicit port and userinfo (
https://[email protected]:8443/x→https://<current>/x). A host reachable only on a non-default port isn't supported; put it behind a reverse proxy on 443/80.
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads
- Opt-in update check (#19). A new Check for new versions of this plugin setting (off by default, option