Security (low): request scheme downgrades https login, reset, admin and moderation URLs to http #9
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Severity: low (likelihood low, impact medium)
Found by: security audit run 3 (commit
01195c8), independently verified in a disposable local WordPress 7.1.3 labFingerprint:
multi-domain-redux:UrlRewriter.rewrite:scheme-replaced-with-request-schemeSummary
UrlRewriter::rewrite is hooked at priority 99 on 21 URL filters (including network_site_url, site_url, home_url, login_url, admin_url). For every URL on a configured host it rebuilds the URL with the current request's scheme (HostResolver::scheme()) and discards the https scheme core chose, whether from FORCE_SSL_ADMIN for 'login'/'admin' schemes or from https home/siteurl stored in the database on a non-SSL request. With the plugin active, on any plain-http request and in CLI or system-cron runs (no HTTPS server variable), these URLs are emitted as http://. That includes the emailed password-reset link (core retrieve_password builds it with network_site_url(..., 'login')), wp_login_url, admin_url and the wp_login_form action. Reproduced in WordPress 7.1.3 with FORCE_SSL_ADMIN: with the plugin skipped core emailed https://network.test/wp-login.php?login=dummyv1&key=...&action=rp; with the plugin active it emailed http://network.test/wp-login.php?... and the front-end login form action changed from https to http. If the victim follows that link or submits that form before HTTPS is enforced (for example, no HSTS), the reset key or password crosses the network in cleartext; core's own http-to-https redirect on wp-login.php fires only after the request has been sent. The same downgrade reaches comment-moderation email: an unauthenticated visitor who submits a comment over plain http to wp-comments-post.php (core does not redirect that endpoint to https, unlike wp-login.php) makes core's moderator notification carry http:// Approve/Trash/Spam admin_url links, an http:// moderation-panel link and an http:// post permalink, and the post-submit 302 Location is http://; with the plugin inactive core emits all of them as https. In the lab this was a real web request on a site whose stored siteurl/home were https, which makes core define FORCE_SSL_ADMIN automatically.
Root cause
src/Rewrite/UrlRewriter.php:61 always uses $this->resolver->scheme() as the output scheme and never consults the input URL's own scheme ($parts['scheme']). That request scheme comes from HostHeader::detectScheme (src/Request/HostHeader.php:94-109), which returns 'http' whenever is_ssl() is false, X-Forwarded-Proto is untrusted or absent, and HTTPS is unset. The plugin thereby overrides the https decision of core's set_url_scheme/get_home_url, including FORCE_SSL_ADMIN. Host::scheme() (src/Config/Host.php:27), which returns https for clearnet hosts, is never used.
Expected behaviour
Rewriting should swap only the host to the visitor's host and never lower the scheme core chose. An https URL from core (FORCE_SSL_ADMIN or stored https URLs) should stay https on clearnet hosts whatever the request scheme; only onion hosts, which are http by design, should be emitted as http.
Source trace
src/Rewrite/HookBindings.php:57(HookBindings::register): rewriteUrlFilter is registered at priority 99 on every URL_FILTERS entry (network_site_url, site_url, home_url, login_url, admin_url, lostpassword_url, ...), after core's set_url_scheme has chosen https. The input arrives from the request context (a plain-http request, or CLI/cron with no HTTPS variable) while core builds a page or reset-email URL.src/Request/HostResolver.php:59(HostResolver::resolve): The output scheme is taken from HostHeader::detectScheme for the current request (memoized per request), independent of the URL being rewritten or of FORCE_SSL_ADMIN.src/Request/HostHeader.php:108(HostHeader::detectScheme): Returns 'http' whenever is_ssl() is false, X-Forwarded-Proto is untrusted or absent, and HTTPS is unset: plain-http requests, CLI and system cron.src/Rewrite/UrlRewriter.php:61(UrlRewriter::rewrite): Returns scheme() . '://' . current host . tail, turning an https input URL (password-reset link, login form action) into http.Conditions
Lab reproduction (local, dummy data only)
Principal: A passive on-path network observer (no account) watching the victim's traffic to a clearnet host of a site that uses FORCE_SSL_ADMIN or stored https URLs. The plugin turned the password-reset email or login form target into http, exposing the reset key or password to that observer.
Observed: With FORCE_SSL_ADMIN emulated and the plugin skipped, core emailed https://network.test/wp-login.php?login=dummyv1&key=<redacted>&action=rp&wp_lang=en_US; with the plugin active the same flow emailed http://network.test/wp-login.php?login=dummyv1&key=<redacted>&action=rp&wp_lang=en_US. wp_login_url, admin_url and the wp_login_form action went from https://network.test/... (plugin skipped) to http://network.test/... (plugin active). With stored https home/siteurl on a non-SSL run, home_url and site_url went from https to http. With HTTPS=on before load the plugin kept https. Network sniffing of a key was not attempted (the lab has no TLS). Comment variant (verifier v10, multisite lab on net2.test, real plain-http POST, stored https siteurl/home so core set FORCE_SSL_ADMIN, is_ssl false): with the plugin active the moderator email contained http://net2.test/wp-admin/comment.php?action=approve|trash|spam&c=7, http://net2.test/wp-admin/edit-comments.php?comment_status=moderated and http://net2.test/2026/10/16/hello-world/, and the 302 Location was http://; with the plugin deactivated every link and the Location were https://. Trackback and pingback paths were not exercised.
Fix
Never lower the scheme: for clearnet hosts keep https when either the input URL or the request is https, while onion hosts keep their fixed http. Apply the same rule in UrlRewriter::urlFor and ContentRewriter, and add a unit regression test that rewriting 'https://<configured>/wp-login.php?...' during an http request returns https.
Suggested change in
src/Rewrite/UrlRewriter.php:Priority rationale