From c403a429043ec721f31dd133cd7c1215d362af49 Mon Sep 17 00:00:00 2001 From: Local Dev Date: Sat, 3 Oct 2026 18:42:45 +0200 Subject: [PATCH] sirius-press: let the sign-in screen move off wp-login.php MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit wp-login.php is the most-requested path on the internet. There is no password here to guess, so this is not about brute force — it is that a wallet login screen answering that path tells every scanner that goes past exactly what is running, and serves a few hundred bots a day a page they cannot use. Setting a login address moves the screen and makes wp-login.php answer 404 through the theme's own template. A 404 and not a redirect: a redirect would hand the new address to the first scanner that asked for the old one. Everything that builds a login URL follows it, because they all go through site_url() or network_site_url() with a wp-login.php path, and there is a wp_redirect backstop for code that builds the URL by hand. Refusing wp-login.php has to happen at wp_loaded and has to end the request there. The web server sends that path to the file directly rather than through index.php, so by the time anything of ours runs we are already inside its require of wp-load.php — rewriting $pagenow changes what WordPress thinks the request is, but it does not stop the script that is already running. What this does not do is rename wp-admin. WordPress has no supported way to, and faking it with a proxy and a filtered admin_url() gets most of the way before breaking plugins quietly. Breaking plugins is the one thing this fork promises not to do, so the setting says as much on the screen rather than implying more than it delivers. --- CHANGELOG.md | 21 ++ .../includes/class-spa-login-url.php | 317 ++++++++++++++++++ .../includes/class-spa-settings.php | 45 +++ .../sirius-press-auth/sirius-press-auth.php | 11 +- 4 files changed, 393 insertions(+), 1 deletion(-) create mode 100644 plugins/sirius-press-auth/includes/class-spa-login-url.php diff --git a/CHANGELOG.md b/CHANGELOG.md index c62cef9..eea7c72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -114,6 +114,27 @@ merge now runs in a scratch worktree and a failed run can be retried. the site's real `Host` header so the rendered page is unchanged and the internal address never reaches the output. The stack sets it by default. +### The login address can move + +- **Sirius Press → Sign-in** now has a *Login address* field. Set it and the + sign-in screen answers at `https://yoursite/`; `wp-login.php` answers + 404, rendering the theme's own 404 page rather than redirecting, because a + redirect would hand the new address to the first scanner that asked for the + old one. Everything that builds a login URL follows — `wp_login_url()`, + logout, lost password, registration, and the redirect `auth_redirect()` + sends a logged-out visitor to `wp-admin` with. +- Not about brute force: there is no password to guess. It is about + `wp-login.php` being the most-requested path on the internet, and a wallet + login screen answering it telling every scanner what runs here. +- **It does not rename `wp-admin`, and will not.** WordPress has no supported + way to do that — the path is built into `ABSPATH . 'wp-admin/'`, into core's + own asset loaders, and into a great deal of plugin JavaScript. A proxy plus + a filtered `admin_url()` gets perhaps nine tenths of the way and then breaks + plugins silently, which is the one thing this fork promises not to do. +- Locked out? `wp option delete sirius_press_login_slug` puts it back, and + `SIRIUS_PRESS_LOGIN_SLUG` in `wp-config.php` overrides the option for anyone + who can still edit a file but not reach a browser. + ### Fixed while building a real site on it - **Turning passwords off made the login screen fail on arrival.** diff --git a/plugins/sirius-press-auth/includes/class-spa-login-url.php b/plugins/sirius-press-auth/includes/class-spa-login-url.php new file mode 100644 index 0000000..dab1356 --- /dev/null +++ b/plugins/sirius-press-auth/includes/class-spa-login-url.php @@ -0,0 +1,317 @@ +set_404(); + } + + status_header( 404 ); + nocache_headers(); + + require_once ABSPATH . WPINC . '/template-loader.php'; + exit; + } + + /** + * Rewrite every URL core builds for wp-login.php. + * + * @param string $url + * @param string $path + * @param string $scheme + * @return string + */ + public static function filter_site_url( $url, $path, $scheme ) { + if ( ! is_string( $path ) || 0 !== strpos( ltrim( $path, '/' ), 'wp-login.php' ) ) { + return $url; + } + + // The caller may have appended a query — "wp-login.php?action=logout". + $query = ''; + $mark = strpos( $url, '?' ); + if ( false !== $mark ) { + $query = substr( $url, $mark ); + } + + return self::url( $scheme ) . $query; + } + + /** + * Catch redirects built by hand rather than through site_url(). + */ + public static function filter_redirect( $location ) { + if ( ! is_string( $location ) || false === strpos( $location, 'wp-login.php' ) ) { + return $location; + } + + return str_replace( site_url( 'wp-login.php', 'login' ), self::url(), $location ); + } + + // ------------------------------------------------------------------ paths + + /** The request path, trimmed of its trailing slash, or null. */ + private static function request_path() { + if ( empty( $_SERVER['REQUEST_URI'] ) ) { + return null; + } + + $uri = rawurldecode( wp_unslash( $_SERVER['REQUEST_URI'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput + $path = wp_parse_url( $uri, PHP_URL_PATH ); + + return is_string( $path ) ? untrailingslashit( $path ) : null; + } + + /** The path WordPress is served from, '' at the document root. */ + private static function home_path() { + return untrailingslashit( (string) wp_parse_url( home_url(), PHP_URL_PATH ) ); + } + + private static function site_path() { + return untrailingslashit( (string) wp_parse_url( site_url(), PHP_URL_PATH ) ); + } + + // --------------------------------------------------------------- warnings + + /** + * Reasons a slug would not do what the operator expects, for the settings + * screen to show before it is saved rather than after. + * + * @return string[] + */ + public static function problems( $slug ) { + $problems = array(); + + if ( '' === self::sanitize( $slug ) && '' !== trim( (string) $slug ) ) { + $problems[] = __( 'A login address is one path segment of lowercase letters, digits and hyphens — and not a name WordPress already uses.', 'sirius-press' ); + return $problems; + } + + $slug = self::sanitize( $slug ); + if ( '' === $slug ) { + return $problems; + } + + if ( is_multisite() ) { + $problems[] = __( 'This site is part of a network, and a network resolves wp-login.php itself. Moving it here would only half work, so it is left alone.', 'sirius-press' ); + } + + $clash = get_page_by_path( $slug, OBJECT, array( 'page', 'post' ) ); + if ( $clash ) { + $problems[] = sprintf( + /* translators: %s: the conflicting post title. */ + __( 'There is already a page at that address — “%s”. The login screen would hide it.', 'sirius-press' ), + get_the_title( $clash ) + ); + } + + if ( defined( 'SIRIUS_PRESS_LOGIN_SLUG' ) ) { + $problems[] = sprintf( + /* translators: %s: the slug set in wp-config.php. */ + __( 'wp-config.php pins the login address to “%s”, which overrides whatever is saved here.', 'sirius-press' ), + esc_html( self::sanitize( SIRIUS_PRESS_LOGIN_SLUG ) ) + ); + } + + return $problems; + } +} diff --git a/plugins/sirius-press-auth/includes/class-spa-settings.php b/plugins/sirius-press-auth/includes/class-spa-settings.php index 17d8c00..d392715 100644 --- a/plugins/sirius-press-auth/includes/class-spa-settings.php +++ b/plugins/sirius-press-auth/includes/class-spa-settings.php @@ -56,6 +56,26 @@ final class SPA_Settings { } update_option( SP_Settings::OPT_OPEN_REG, ! empty( $_POST['sirius_open_registration'] ) ); + + $raw = isset( $_POST['sirius_login_slug'] ) ? sanitize_text_field( wp_unslash( $_POST['sirius_login_slug'] ) ) : ''; + $slug = SPA_Login_URL::sanitize( $raw ); + $slug_issues = SPA_Login_URL::problems( $raw ); + + if ( $slug_issues ) { + $errors = array_merge( $errors, $slug_issues ); + } elseif ( $slug !== SPA_Login_URL::slug() ) { + if ( '' === $slug ) { + delete_option( SPA_Login_URL::OPT_SLUG ); + $notes[] = __( 'The login screen is back at wp-login.php.', 'sirius-press' ); + } else { + update_option( SPA_Login_URL::OPT_SLUG, $slug ); + $notes[] = sprintf( + /* translators: %s: the new login URL. */ + __( 'The login screen has moved to %s. Save it somewhere — wp-login.php now answers 404.', 'sirius-press' ), + home_url( '/' . $slug ) + ); + } + } } $stranded = self::accounts_without_wallets(); @@ -91,6 +111,31 @@ final class SPA_Settings { ); echo ''; + echo '' . esc_html__( 'Login address', 'sirius-press' ) . ''; + printf( + '%s ', + esc_html( trailingslashit( home_url( '/' ) ) ), + esc_attr( SPA_Login_URL::slug() ), + esc_attr__( 'wp-login.php', 'sirius-press' ) + ); + echo '

'; + esc_html_e( 'Move the sign-in screen somewhere other than wp-login.php. Leave it empty to keep the usual address.', 'sirius-press' ); + echo ' '; + esc_html_e( 'This is not about brute force — there is no password to guess — but wp-login.php is the most-requested path on the internet, and answering it tells every scanner what is running here. Note that it does not move wp-admin: WordPress has no supported way to rename that, and pretending otherwise breaks plugins quietly.', 'sirius-press' ); + echo '

'; + if ( SPA_Login_URL::slug() ) { + echo '

'; + printf( + /* translators: %s: the current login URL. */ + esc_html__( 'Signing in now happens at %s.', 'sirius-press' ), + '' . esc_html( home_url( '/' . SPA_Login_URL::slug() ) ) . '' + ); + echo ' '; + esc_html_e( 'If you lose it, `wp option get sirius_press_login_slug` on the server will tell you, and `wp option delete sirius_press_login_slug` will put the login back.', 'sirius-press' ); + echo '

'; + } + echo ''; + echo ''; submit_button(); echo ''; diff --git a/plugins/sirius-press-auth/sirius-press-auth.php b/plugins/sirius-press-auth/sirius-press-auth.php index 385f928..47c4c05 100644 --- a/plugins/sirius-press-auth/sirius-press-auth.php +++ b/plugins/sirius-press-auth/sirius-press-auth.php @@ -18,10 +18,19 @@ defined( 'ABSPATH' ) || exit; -define( 'SIRIUS_PRESS_AUTH_VERSION', '0.1.1' ); +define( 'SIRIUS_PRESS_AUTH_VERSION', '0.2.0' ); define( 'SIRIUS_PRESS_AUTH_DIR', plugin_dir_path( __FILE__ ) ); define( 'SIRIUS_PRESS_AUTH_URL', plugin_dir_url( __FILE__ ) ); +/* + * The login address is decided before everything else, and outside the + * `plugins_loaded` callback below, because it has to hook `plugins_loaded` + * itself at a priority earlier than that callback runs. It depends on nothing + * but WordPress, so there is nothing to wait for. + */ +require_once SIRIUS_PRESS_AUTH_DIR . 'includes/class-spa-login-url.php'; +SPA_Login_URL::hooks(); + add_action( 'plugins_loaded', function () {