A post appeared that nobody on your team wrote. A client says they did not install that plugin. You got a “Password changed” or “New admin email” notice you did not trigger, or a security plugin reports a login from a country where no one works. Whatever the clue, the conclusion is the same: someone else is logged in as one of your users.
A single compromised account is not always a fully hacked site, but it can become one within minutes if the account is an administrator. The order of work matters. First you cut off every way the intruder is signed in, then you secure the email account that could hand them a new password, and only then do you investigate what they did. This guide covers that sequence for site owners, agencies and freelancers looking after client sites. If the site is already serving spam or malware, or you find code changes, switch to the first-hour hacked site guide (FG-05) after Step 2.
What is happening
WordPress keeps you logged in with signed cookies, backed by session tokens stored in the database. Anyone holding a valid cookie is “you” until the session expires or is destroyed. Separately, Application Passwords (built into WordPress since 5.6) let apps and scripts authenticate to the REST API and XML-RPC without the main password, and they keep working after the main password changes.
| Way in |
Survives a password change? |
How to cut it off |
| Login cookie in the intruder’s browser |
No. The cookie includes a fragment of the password hash, so a new password invalidates it. Destroy sessions anyway, to be explicit. |
wp user session destroy |
| Application Password |
Yes |
wp user application-password delete |
| Password reset through the user’s email |
Yes, if they control the inbox |
Secure the email account |
| A second admin account they created |
Yes |
Review all users (Step 4) |
| A backdoor file or plugin |
Yes |
File review (FG-05, FG-26) |
The usual causes are a password reused from another breached site, a phished login, malware on the user’s own computer stealing saved browser sessions, or an email account takeover followed by a WordPress password reset. Brute force is less common for strong passwords; if your logs show it, the brute-force guide (FG-82) covers that side.
Before you start
- Access you need: an administrator account you trust (not the compromised one), SSH or your host’s file manager, and ideally access to the email account behind the compromised login, or contact with the person who owns it.
- Backup: take a backup of files and database now. It preserves evidence of what the intruder changed.
- How to run commands: connect over SSH and change into the site folder, the one with
wp-config.php. WP-CLI is wp on most servers and wp-cli on some managed hosts. Every command below runs there. Without SSH, you can do the same things through Users in wp-admin, though it is slower and some evidence is harder to see.
Step 1: Contain the account (the first five minutes)
Identify the account and capture its sessions
wp user get USER_LOGIN --fields=ID,user_login,user_email,roles
wp user session list USER_ID --fields=login_time,expiration_time,ip,ua
Placeholders: USER_LOGIN is the username of the affected account; USER_ID is the numeric ID the first command prints. The second command lists active sessions with when they started, when they expire, the IP and the browser user agent. Good output: sessions from IPs and browsers the real user recognizes. Bad output: a session from an unknown IP, often a hosting provider or VPN range, or a user agent unlike anything the person uses. Copy this output into your notes now; it is your best evidence and it disappears in the next step. Also check that user_email is still the real person’s address; attackers often change it to lock the owner out.
Reset the password without exposing it
wp user update USER_ID --prompt=user_pass
--prompt=user_pass makes WP-CLI ask for the new password interactively instead of taking it on the command line, so it never lands in your shell history. Paste a long, randomly generated password from a password manager. Good output: “Success: Updated user USER_ID.” If the attacker changed the account’s email, fix it in the same way with --user_email=REAL_EMAIL, where REAL_EMAIL is the owner’s verified address. Do not send a reset link to an email address you have not confirmed is secure.
Destroy every session
wp user session destroy USER_ID --all
--all destroys every session for that user rather than just the newest. Good output: “Success: Destroyed all sessions.” Run wp user session list USER_ID again; it should return an empty table.
Revoke Application Passwords
wp user application-password list USER_ID --fields=uuid,name,created,last_used,last_ip
wp user application-password delete USER_ID --all
The first command shows each application password with its name, creation time, last use and last IP (times are Unix timestamps). Bad output: an entry you do not recognize, or one created around the time of the compromise; attackers add these as a quiet way back in. Note the details, then delete them all with the second command. --all removes every application password for that user; to delete just one, pass its UUID instead. Good output: “Success: Deleted all application passwords.” Legitimate integrations will need a fresh application password afterward.
Step 2: Secure the email account behind the login
Whoever controls the inbox controls the WordPress account, because “Lost your password?” sends the reset link there. An email takeover is one of the most common roots of a WordPress account compromise, and it is easy to miss because the site itself was never attacked.
- Change the email account’s password, and sign out all other sessions from the provider’s security settings.
- Check forwarding rules and filters. A rule that forwards or deletes messages containing “password” or “WordPress” lets an attacker reset your site password silently. This is the single most overlooked step.
- Review recovery phone numbers, recovery emails, app passwords and connected third-party apps on the email account, and remove anything unfamiliar.
- Turn on two-factor authentication for the email account, with an authenticator app or security key.
- If the account owner reused their password anywhere, change it there too, starting with banking, hosting, domain registrar and DNS accounts.
If the person’s computer may be infected (for example, sessions were stolen despite 2FA), have them scan it before logging in again; otherwise the new password and cookies are stolen the same way.
Step 3: Review what the account did
If you have an activity log plugin
Plugins such as WP Activity Log, Simple History and Stream record logins, content edits, settings changes and plugin installs with the user, IP and time. Filter the log by the compromised user and by the IPs you captured in Step 1. If you have no activity log, the checks below reconstruct the same picture from WordPress data.
Users created or promoted
wp user list --fields=ID,user_login,user_email,roles,user_registered --orderby=registered --order=DESC
This lists every user, newest first. Bad output: any account registered after the compromise began, or an existing account whose role is now administrator when it should not be. Delete rogue accounts with wp user delete ROGUE_USER_ID --reassign=YOUR_USER_ID, where ROGUE_USER_ID is the account to remove and YOUR_USER_ID is a trusted user who should own any content it created. Accounts that do not show in this list but exist in the database are covered in the reinfection guide (FG-26).
Content changed
wp post list --post_type=any --post_status=any --author=USER_ID --orderby=modified --order=DESC --fields=ID,post_type,post_status,post_title,post_modified --posts_per_page=30
--post_type=any and --post_status=any include pages, custom post types, drafts and private posts. --author limits it to the compromised account. Bad output: posts or pages modified at times the real user was not working, new pages with spam or links, or drafts you did not write. Note that edits to posts owned by other authors do not show here; repeat without --author to see all recent changes, and compare revisions in the editor’s revision history.
Plugins and themes installed or changed
wp plugin list --fields=name,status,version
ls -lt wp-content/plugins | head -15
ls -lt wp-content/mu-plugins 2>/dev/null
The first lists plugins; the ls -lt commands sort plugin folders by modification time, newest first, and check the must-use folder, which loads code without appearing as a normal plugin. Bad output: a plugin you do not recognize, a file manager or code-snippet plugin installed recently, or anything new in mu-plugins. An admin who can install plugins can run any code, so any of these means the account compromise has become a site compromise: go to FG-05.
Settings changed
wp option get admin_email
wp option get users_can_register
wp option get default_role
wp option get siteurl
Good output: your admin email, registration as you set it, subscriber (or customer on WooCommerce) as the default role, and your own URL. Bad output: an unknown admin email, registration switched on with a default role of administrator, or a changed site URL. On WooCommerce stores, also open the payment settings and check the bank account details for direct bank transfer and the receiving account for any payment gateway; quietly swapping these is a known way to steal money without touching code.
Step 4: Rotate salts to log everyone out
If you are not sure which accounts were affected, or the compromised account was an administrator, log out every user at once by rotating the security keys and salts in wp-config.php. Every login cookie is signed with these, so new keys make all existing cookies invalid.
wp config shuffle-salts
This replaces the keys and salts in wp-config.php with fresh random values. Good output: “Success: Shuffled the salt keys.” Everyone, including you, has to log in again. If wp-config.php is not writable, generate new values at the WordPress.org secret key service and paste them in by hand. Rotating salts does not revoke Application Passwords, which is why Step 1 handles those separately.
Step 5: Turn on two-factor authentication
With the intruder out, make a stolen password useless next time. Install a 2FA plugin (the Two Factor plugin maintained by WordPress contributors, Wordfence Login Security, or the login security module in your existing security suite) and require it for administrators, editors and shop managers. Prefer authenticator apps or security keys over email codes, since email was a likely way in. Save backup codes in your password manager; the locked-out guide (FG-20) covers what to do if you lose them.
Step 6: Apply least privilege
Most accounts do not need to be administrators. Every extra admin is another password that can install plugins and run code.
| Role |
Can do |
Give it to |
| Administrator |
Everything, including installing plugins and editing users |
One or two people who maintain the site |
| Editor |
Publish and edit all content |
Content managers |
| Author |
Publish and edit their own posts, upload media |
Regular writers |
| Contributor |
Write drafts, no publishing or uploads |
Guest writers |
| Shop Manager (WooCommerce) |
Manage orders, products and store settings, not plugins |
Store staff |
Find and demote unnecessary admins with wp user set-role USER_ID editor, where USER_ID is the account to change and editor is the new role. Also consider adding define( 'DISALLOW_FILE_EDIT', true ); to wp-config.php, which removes the built-in theme and plugin code editors, so a hijacked admin session cannot edit PHP from the browser.
Advanced: the shared login problem for agencies and clients
A single “admin” login shared by the client, the agency and a couple of freelancers is the most common setup behind compromises nobody can trace. When everyone uses one account, you cannot tell who did what, you cannot remove one person without locking out all of them, and the password ends up in emails, chat threads and spreadsheets.
- One person, one account. Create named accounts for every human, including each agency staff member, so sessions, logs and revocation all work per person.
- Agency access ends with the work. Demote or delete agency accounts when a project ends, or keep one maintenance account protected with 2FA and an agency-controlled mailbox.
- Clients own the top account. The site owner should hold an administrator account tied to an email address they control, so access survives a change of agency.
- Share secrets properly. Use a password manager’s sharing feature, never email or chat. Hosting, registrar and DNS logins deserve the same treatment as WordPress.
- Audit quarterly. Run
wp user list --role=administrator on every client site and remove anyone who no longer needs access.
For agencies managing many sites, the same session and application-password commands work in a loop over each site with --path=/PATH/TO/SITE, where /PATH/TO/SITE is each install’s root folder, which makes a fleet-wide response to a leaked staff password practical.
When to contact your host
Open a ticket if you found unknown plugins or files, cannot get SSH or database access, or need server logs older than you can see. Include:
- The affected username and the UTC time window of the suspicious activity.
- The IPs and user agents from the session list you captured in Step 1.
- Anything the account changed: users, plugins, content, settings.
- What you have already done (password reset, sessions destroyed, salts rotated) and when.
Ask for access logs covering that window, filtered to wp-login.php, /wp-admin/ and /wp-json/ requests from those IPs. Our diagnostic tools can check your site from outside, and if the site is visibly hacked, start at the Emergency Room.