Skip to content
Corex

LoginRouteGuard

Corex\Config\Security\LoginProtection\LoginRouteGuard · class · Config layer

Hides the default login and admin endpoints behind a custom slug, without moving core files. The hiding is only obscurity — it cuts automated probing, it is not an access control. The throttling in LoginProtectionEnforcer is what actually defends credentials. Obscurity that announces itself is worth nothing, and the previous implementation announced itself twice (both reproduced live, DECISIONS #140): core’s own wp_redirect_admin_locations still redirected /login straight to the secret slug, and the “hidden” endpoint answered with a 2.5 KB wp_die page where a real miss returns the ~80 KB theme 404 — telling a scanner “something is hidden here” on response size alone. So the rules here are: let WordPress produce the 404 itself, and make sure nothing anywhere still points at the default endpoint.

__construct(LoginProtectionSettings $settings, bool $unguarded)

Section titled “__construct(LoginProtectionSettings $settings, bool $unguarded)”

Decide once what this request is, and get out of WordPress’s way. Runs before the main query, so a hidden request is re-pointed at {@see NOWHERE} and core 404s it on its own terms.

entryPointFor(string $path, bool $isAdmin): string

Section titled “entryPointFor(string $path, bool $isAdmin): string”

What this URL is, before the login state is knowable. Pure, so the rules are testable without a request. This answers “is this URL the slug, the default endpoint, or unrelated?” — a question that needs no login state. Whether the admin area is hidden is a different question with a different answer, asked later by {@see hidesAdminArea()} once pluggable.php has loaded. Two questions, two methods; the bug this replaces was two methods answering the same question differently.

hidesAdminArea(bool $isAdmin, bool $loggedIn, bool $ajax, string $script, string $path): bool

Section titled “hidesAdminArea(bool $isAdmin, bool $loggedIn, bool $ajax, string $script, string $path): bool”

Whether the admin area must be hidden from this visitor. Asked on wp_loaded because it needs a login state, which does not exist at capture time — pluggable.php loads after plugins_loaded.

Keep the slug from an admin request that is going to be hidden, until it is answered. The request is answered on wp_loaded, and core does not always wait that long. The Customizer checks the visitor on setup_theme and calls auth_redirect(); that redirect was built from the login URL, which is rewritten to the slug, so /wp-admin/customize.php handed the hidden address to anyone who asked for it (reproduced 2026-10-08, with the report in {@see isRegistrationForwarder()}). From here until the 404 is rendered the URL filters leave the default address alone, so anything else that bounces this request to the login early sends it to wp-login.php, which is hidden too. The Customizer’s own check is taken off, so this request reaches the 404 every other hidden admin address gets. The rewriting is back on for the 404 itself: a real missing page links to the slug wherever the theme links to the login, and this one must not differ.

Act on the decision {@see captureRequest()} already made.

Strip the admin-ness that would otherwise leak into a hidden admin request’s 404. WP_ADMIN is a constant and cannot be unset, so is_admin() stays true while we render the theme’s 404 and core keeps taking admin branches it should not. Each one has to be undone by hand. Only applied when the hidden request really is an admin one — doing it to a front-end request would strip things a genuine front-end 404 has, differing in the other direction. KNOWN LIMITATION: this narrows the difference, it does not erase it, and the difference is bigger than this comment used to claim. wp_should_load_separate_core_block_assets() returns false on is_admin() before its own filter runs (wp-includes/script-loader.php), which is genuinely unreachable from here, and it cuts both ways. Measured against WordPress 7.1: +25KB corex-{success-message,flow,subscribe,cta-flow,survey,carousel,form,drawer,tabs} inline styles, present here and on no real 404 — with separate assets off, every enqueued block style prints, not only the ones a rendered block asked for. -16KB corex-navigation, wp-block-library and wp-block-search inline styles, present on a real 404 and not here. The response is styled and visually indistinguishable, and the two errors happen to be of similar size, so the byte counts stayed close enough that a 5% assertion passed for months without either being known. They are not identical and they are not stable: WordPress 7.1 moved the balance on its own and the gap went to 36%. So a probe that looks for corex-survey-style-inline-css on a “missing page” can still tell this endpoint from one that was never there. Closing that needs a way to make core take the front-end asset path on an is_admin() request; there is no such hook. Recorded in DECISIONS #222 with the measurements. Hiding /wp-login.php — the endpoint that actually identifies a hidden login — IS byte-identical; see the spec for the measured sizes. Public only so it can be tested. render404() exits, so nothing downstream of it is observable from a test; this hook surgery is the part that has to be right, and the defect it fixes shipped precisely because nothing could see it.

Enqueue what wp_common_block_scripts_and_styles() skipped. Public only so it can be hooked by name and asserted; see restoreBlockStyles().

Point a URL that references the default login at the custom slug instead.

Point the login address inside the multisite welcome email at the custom slug. Separate from filterLoginUrl because this filter carries an entire message body, not a URL — parsing it as one yields nonsense. A plain replacement of the script name is what fits here.

filterSiteUrl(mixed $url, mixed $path): mixed

Section titled “filterSiteUrl(mixed $url, mixed $path): mixed”

The custom login URL, honouring the site’s permalink and trailing-slash settings.