跳到正文
DEV Community· AumCreate·· 3 小時前AI 評分57

WordPress 登入加固:應先處理哪些問題,以及何時更改登入網址

Hardening WordPress logins: what to fix first, and where the login URL actually belongs

AI 導讀

AumCreate 提出 WordPress 登入加固次序,建議先審核管理員權限,再處理應用程式密碼、XML-RPC 及自訂端點等可繞過登入表單的入口。指南亦建議啟用雙重驗證,並在網絡邊緣及應用程式層限流;只有紀錄顯示預設登入端點遭大量存取時,才把更改登入網址列為最後一步。

正文

If you inherit a WordPress site, the login URL is often already renamed and the administrator list is often still untouched. The rename takes five minutes and looks like progress in a screenshot. Auditing who can install a plugin takes an afternoon and produces nothing you can show anyone.

That order is backwards. Below is a sequence that holds up, and where the login URL actually fits.

Capabilities decide what a stolen password is worth

Start with what an account can do. A compromised editor can publish junk. A compromised administrator can install a plugin that mails the database somewhere.

List the administrators, then check for accounts that hold manage_options without being in the role:

wp user list --role=administrator --fields=ID,user_login,user_email,user_registered

wp eval 'foreach ( get_users() as $u ) { if ( user_can( $u, "manage_options" ) ) { echo $u->user_login . "\n"; } }'

The second command catches custom roles that accumulated capabilities over time.

Then:

  • Move anyone who writes but does not administer down to editor. Editors cannot install plugins or switch themes.
  • Delete accounts that belong to nobody. Reassign their content first: wp user delete <id> --reassign=<other_id>.
  • Give contractors a named account with an end date instead of a shared login. Shared accounts make the log useless, because you cannot tell who did what.
  • Check for application passwords on accounts that should not have them. They are a separate credential that never touches your login form.

This step is the only one that limits the damage after a password leaks. Everything below reduces the chance of the leak.

Two-factor, and the doors that skip the login form

A second factor on wp-login.php protects interactive browser logins. It does not protect:

  • application passwords, which are a core feature and authenticate over REST with no second factor;
  • XML-RPC, if it is enabled and a client sends credentials;
  • any custom endpoint you wrote that checks a password itself.

Settle those before you tell anyone 2FA is on. If nothing uses application passwords:

<?php
// wp-content/mu-plugins/no-app-passwords.php
add_filter( 'wp_is_application_passwords_available', '__return_false' );

If nothing uses XML-RPC:

<?php
// wp-content/mu-plugins/no-xmlrpc.php
add_filter( 'xmlrpc_enabled', '__return_false' );

That filter disables the XML-RPC methods that require authentication. Pingback methods are governed separately by other hooks, so do not assume the endpoint is gone — check it.

For the factor itself, TOTP with recovery codes printed and stored offline is the boring option that keeps working. Whatever you choose, confirm that someone with WP-CLI access can reset enrolment. A lost phone should not turn into a support incident at 2am.

Throttle where the attempt is visible

Edge throttling handles volume. Application throttling handles the specific endpoint. You want both, because an attacker who gets blocked on wp-login.php tries xmlrpc.php next.

At nginx level, a rate limit in the http block and applied to the login location (the socket path has to match your pool):

# http context
limit_req_zone $binary_remote_addr zone=wplogin:10m rate=10r/m;

# server context
location = /wp-login.php {
    limit_req zone=wplogin burst=5 nodelay;
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_pass unix:/run/php/php-fpm.sock;
}

At application level, count failures per source and short-circuit before the password is checked:

<?php
/**
 * Plugin Name: Login attempt throttle
 */

function mylt_key(): string {
    $ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
    return 'mylt_' . md5( $ip );
}

add_filter( 'authenticate', function ( $user, $username, $password ) {
    if ( empty( $username ) || empty( $password ) ) {
        return $user;
    }
    if ( (int) get_transient( mylt_key() ) >= 10 ) {
        return new WP_Error( 'mylt_blocked', 'Too many failed attempts. Try again in 15 minutes.' );
    }
    return $user;
}, 30, 3 );

add_action( 'wp_login_failed', function () {
    $key   = mylt_key();
    $fails = (int) get_transient( $key );
    set_transient( $key, $fails + 1, 15 * MINUTE_IN_SECONDS );
} );

add_action( 'wp_login', function () {
    delete_transient( mylt_key() );
} );

Two things to get right before you ship it:

  • Behind a CDN or load balancer, REMOTE_ADDR is the proxy address and you will lock out every user at once. Throttle at the edge in that setup, or read a forwarded header only when the request comes from a proxy you control.
  • Apply the same rule to the REST application-password endpoint and to XML-RPC. Otherwise the counter just redirects the attacker to another door.

The login URL question

Renaming wp-login.php filters automated noise out of your logs. That has value. It is not access control: a valid credential still logs in, XML-RPC and REST are untouched, and an unhandled rewrite breaks password-reset links and any integration that assumed the default path.

Before you act on that noise, it helps to know what it is made of. Content crawlers and login hammering look the same in a raw access log until you parse them apart. AumCrawl — free on wordpress.org — shows which AI crawlers are fetching your site and lets you allow or block them one at a time, so what is left in the login column is the traffic actually worth reacting to (plugin page).

Put the rename last, and only when the logs show the default endpoint being hit hard. If you do rename it, write the recovery path into the handover notes — wp user update <login> --user_pass=<new> works regardless of what the login URL is, and that is the thing you will need when a rewrite rule misfires.

What each control actually covers

Control Reduces Does not cover
Least-privilege roles Damage from a stolen account Credential theft
2FA on the login form Reuse of a leaked password Application passwords, XML-RPC, custom endpoints
Disabling app passwords and XML-RPC Those two bypass routes The login form
Rate limiting Online guessing at scale A password already in a dump
Renaming the login URL Bot noise on one path Anything targeted
Logging failed logins Nothing by itself Tells you when to do the rest

Do the accounts first. If the log is still a wall of POSTs to the login endpoint afterwards, you now have the evidence to justify the rename instead of guessing.

來源:DEV Community · dev.to