Guide
WordPress logout you didn't perform — detecting session hijacking
1. Problem
You were editing a post, hit Update, and WordPress bounced you to /wp-login.php with the message "Your session has expired. Please log in again." You log back in, go back to work, and within an hour it happens again. Or you opened the dashboard this morning and your remembered session was simply gone — even though you ticked "Remember Me" two days ago. Sometimes you see a banner saying you have been logged out from another location.
If you are searching "wordpress logged me out" or "wordpress logged out unexpectedly multiple times," this is a WordPress logout you didn't perform, and the question is not how to stop the popup. The question is whether someone else triggered it. Repeated unexpected logouts are one of the few user-visible artifacts of session hijacking on WordPress, and the platform does almost nothing to help you tell the difference between a benign session expiry and an attacker forcing you out so they can keep their own session alive.
The evidence that distinguishes the two cases is in the logs. Specifically, in the pattern of auth.logout events around your account, correlated with auth.attempt successes from new IP addresses and any wp.state_change touching authentication keys.
2. Impact
A single unexpected logout is annoying. A pattern of them is a security incident in progress.
Concrete impact when an admin session is compromised:
- Silent privilege use. An attacker with an active admin cookie can install plugins, edit
functions.php, create new admin users, and exfiltrate customer data. They do not need your password again — the session cookie is the password. - Persistence implants. Competent attackers plant a way back in: a new admin user with an innocuous name, a malicious plugin, a backdoor in a theme file, or a scheduled task that re-creates access.
- Customer data exposure. WooCommerce stores expose customer addresses, order history, and partial payment metadata to admins. A hijacked admin session is a data breach under GDPR.
- SEO and reputation damage. Spam injection, redirect malware, and SEO poisoning are common monetization paths. The damage outlives the session.
Detecting the hijack at the logout event — not after the malicious plugin is installed — is the difference between an incident and an outage.
3. Why It’s Hard to Spot
WordPress was not built to surface this. The platform treats every logout as a routine event, and the user-facing UI gives you no way to tell apart "your cookie expired" from "your session was destroyed by an attacker."
- The logout screen lies by omission. "Your session has expired. Please log in again" is the same message whether the cookie timed out, you logged out in another tab, or someone called
wp_destroy_other_sessions()from a hijacked admin context. - Users tab is shallow.
wp-admin/users.phpshows a "Sessions" link only on your own profile. There is no per-session metadata — no IP, no user-agent, no creation timestamp surfaced in the UI. - No login-from-new-location alerts by default. Unlike Google or GitHub, WordPress does not email you when a successful login occurs from a new IP. A hijacker who steals a valid cookie can use it from anywhere.
- Session tokens are stored opaquely. Session metadata lives in
wp_usermetaundersession_tokensas a serialized PHP array. Not human-readable withoutwp evalor a plugin. - 2FA does not protect existing cookies. 2FA validates
auth.attempt, not the session that follows. - Most hosts do not log access by user. Web server logs show IPs hitting
/wp-admin/. They do not tell you which authenticated user that request belonged to.
The result: WordPress is structurally bad at telling you that someone else is also using your account. The clearest signal you get is a logout you did not request — and that signal is buried in noise unless you are watching for it.
4. Cause
WordPress authentication is cookie-based. After you log in successfully, WordPress writes a signed cookie that includes your username, an expiration timestamp, and a hash. The hash is signed using two secrets stored in wp-config.php: AUTH_KEY and AUTH_SALT (and the matching LOGGED_IN_ and SECURE_AUTH_ pairs). On every subsequent request, WordPress validates the cookie by recomputing the hash with those keys.
A logout — or a forced logout — produces an auth.logout signal. There are four ways an auth.logout is generated:
- User clicks "Log Out". Normal, expected, paired with a deliberate user action.
- Cookie expiration. The cookie's timestamp is past. WordPress destroys the session and emits the signal. Default lifetime is 48 hours, or 14 days with "Remember Me".
wp_destroy_other_sessions()is called. WordPress invalidates all sessions except the current one. This is what happens when you change your password from the profile page — and what an attacker triggers to keep their session while killing yours.- Auth keys rotated. If
AUTH_KEYorAUTH_SALTchanges inwp-config.php, every existing cookie becomes invalid. Every session ends in anauth.logoutevent. This is sometimes intentional, often a side effect, and occasionally an attacker covering their tracks.
The hijacking pattern looks like this in auth.logout data:
- Multiple
auth.logoutevents for the same user account within a short window. - The IP address and user agent of the logout do not match any of that user's recent logins.
- Just before or just after the logout, there is a successful
auth.attemptfor the same user from a new IP, often a new country or hosting-provider ASN. - Sometimes a
wp.state_changeevent firing on thewp_optionsrow holdingauth_keyor on theusermetasession_tokensrow.
Each of those individually is noise. Together they are a hijack signature.
5. Solution
5.1 Diagnose (logs first)
The diagnostic move is to reconstruct the session timeline for the affected user from logs. You are looking for auth.logout events that lack a matching user click, paired with auth.attempt successes from unfamiliar IPs.
1. Pull authentication events from the audit log.
If you have an audit-log plugin (WP Activity Log, Simple History, Stream) the events live in their own tables or in a CSV export. Without one, you have web server logs and PHP error logs, and you reconstruct from POST requests to wp-login.php and /wp-admin/profile.php:
# Successful logins (302 redirect away from wp-login.php)
grep -E 'POST .*/wp-login\.php' /var/log/nginx/access.log \
| grep ' 302 ' \
| awk '{print $1, $4, $7, $NF}'
# Logout events (the wp-login.php?action=logout endpoint)
grep -E 'GET .*/wp-login\.php\?action=logout' /var/log/nginx/access.log \
| awk '{print $1, $4, $7}'
Each wp-login.php 302 produces an auth.attempt signal (success or failure depending on the redirect target). Each logout request produces an auth.logout signal.
2. Cross-check against PHP error and slow logs.
# Look for session destruction calls and user meta writes
grep -E 'destroy_other_sessions|wp_destroy_all_sessions' \
/var/log/php-fpm/error.log
Any hit there means code, not a user action, killed sessions. That code path firing for a non-admin context is suspicious. Each call corresponds to one or more auth.logout events.
3. Inspect session tokens directly.
# Show all active sessions for the user, including IPs and user-agents
wp user meta get USERNAME session_tokens --format=json | jq .
Each entry includes expiration, ip, ua, and login (creation time). Two active sessions for one admin user, originating from different countries, is the hijack confirmed. This data populates the same auth.attempt and auth.logout events Logystera tracks.
4. Check for auth-key rotation.
# Compare current AUTH_KEY against any backup of wp-config.php
diff <(grep AUTH_KEY /var/www/html/wp-config.php) \
<(grep AUTH_KEY /backup/wp-config.php)
A change here invalidates every cookie at once and produces a flood of auth.logout signals across all users. If you did not rotate the keys yourself, someone else did. Auth-key rotation by an attacker is the giveaway that they wanted to silently end every other session — including admin sessions they could not see.
5. Correlate with auth.attempt from new IPs.
For every unexpected auth.logout for an admin user, look for an auth.attempt (success) for that same user within the surrounding 30 minutes from an IP not seen for that user before. If your audit data has it, also compare ASN. A login from a residential ISP followed minutes later by a successful login from a hosting-provider ASN is the canonical hijack pattern.
The signal symmetry: auth.logout tells you the session ended, auth.attempt tells you who started a new one, and the gap between them is the attacker's working window.
5.2 Root Causes
(see root causes inline in 5.3 Fix)
5.3 Fix
Order of operations matters. Sessions and credentials before everything else, then artifacts.
- Force-logout every session, immediately. Rotate
AUTH_KEY,AUTH_SALT,LOGGED_IN_KEY,LOGGED_IN_SALT,SECURE_AUTH_KEY,SECURE_AUTH_SALT,NONCE_KEY,NONCE_SALTinwp-config.php. Use the official generator athttps://api.wordpress.org/secret-key/1.1/salt/. Every existing cookie dies on the next request — produces a wave of legitimateauth.logoutsignals. - Cause → signal: stolen cookie still in use →
auth.attempt(success) from attacker IP → after rotation, attacker's next request produces failed cookie validation, no longer authenticated. - Reset every admin password. Do not just reset yours. Any admin account is a re-entry point. Use
wp user reset-password $(wp user list --role=administrator --field=ID). - Cause → signal: phished or leaked admin password → repeated
auth.attemptsuccesses from attacker IP. Reset breaks reuse. - Audit admin users.
wp user list --role=administrator --fields=ID,user_login,user_email,user_registered. Look for accounts you did not create. Attackers favour usernames likeadmin2,wp-support, or display names that mimic real staff. - Cause → signal: persistence via new admin user →
wp.state_changeon thewp_userstable at the time of the hijack window. - Enforce 2FA on every admin account. A WordPress 2FA plugin (Two Factor, Wordfence Login Security) blocks the
auth.attemptpath even if the password leaks again. 2FA does not protect already-issued cookies, which is why the salt rotation has to happen first. - Check for malicious plugins or
mu-plugins.wp plugin list --status=activeandls -la wp-content/mu-plugins/. Plugins that drop intomu-pluginsactivate silently and produce no admin-screen entry. Each plugin file change should have a matchingwp.state_changesignal — anything inmu-plugins/without one is almost certainly hostile. - Cause → signal: backdoor planted via hijacked admin →
wp.state_changeon plugin file write. - Review scheduled tasks.
wp cron event list. Look for events that recreate users, fetch remote payloads, or calleval. Hijackers often plant a cron job that re-installs their backdoor if you remove it. - Check
wp-config.phpand.htaccessfor unfamiliar additions: redirects,auto_prepend_filedirectives,define('DISALLOW_FILE_EDIT', false)reverted, base64-encoded blocks. Each of these should have left awp.state_changeevent when written. - Force HTTPS-only and HttpOnly cookies. Set
define('FORCE_SSL_ADMIN', true)and confirm cookies are flaggedSecureandHttpOnly. A site served over plain HTTP leaks cookies on every public network — it is the single most preventable hijack vector.
If your stack ran on HTTP at any point during the window, assume the cookie was sniffed and act accordingly.
5.4 Verify
The verification target is straightforward: no more unexpected auth.logout events for admin users, and no successful auth.attempt events from unfamiliar IPs.
auth.logoutbaseline. After the salt rotation flushes everyone out once, a healthy WordPress site producesauth.logoutevents at a rate that matches deliberate user activity — typically a small handful per admin user per day, paired one-to-one with the user's working IP.auth.attemptbaseline. Successfulauth.attemptevents for an admin should originate from one or two known IPs over a 24-hour window. New ASNs are rare.- Greps to run for 24–48 hours after remediation:
# No new logout events from unfamiliar IPs
grep 'wp-login.php?action=logout' /var/log/nginx/access.log \
| awk '{print $1}' | sort -u
# No new successful logins from unknown ASNs
grep -E 'POST .*wp-login\.php' /var/log/nginx/access.log \
| grep ' 302 ' | awk '{print $1}' | sort -u
The IP set should be small and known.
- Session tokens are clean.
wp user meta get USERNAME session_tokens --format=json | jq '. | length'should match the number of devices the user actually uses. Running it daily for a week and seeing a stable, expected count is the durable signal. - No new
wp.state_changeon auth keys, admin users, ormu-pluginsunless you triggered them yourself. This is the absence-of-signal verification.
If 48 hours pass with auth.logout rate matching expected user behavior, no auth.attempt from new IPs, and no wp.state_change on sensitive paths, the hijacker no longer has access.
6. How to Catch This Early
Fixing it is straightforward once you know the cause. The hard part is knowing it happened at all.
This issue surfaces as auth.logout.
The reason hijacks run for hours or days before anyone notices is that WordPress treats auth.logout, auth.attempt, and wp.state_change as routine events. Individually they are. In combination — the same admin user logged out from one IP and logged in from another in the same 30-minute window — they are a signature, and a signature is exactly what a log-driven detection system finds.
The failure mode is not "can I find this in the logs after the fact." You can. The failure mode is that nobody looks until something obvious breaks, and by then the attacker has already planted persistence and moved on. Default WordPress sends no email on a new-IP login. Default uptime checks see only the home page. Default hosting dashboards normalize "logged in successfully" to a count.
This pattern surfaces as auth.logout correlated with auth.attempt from a new IP, frequently followed by wp.state_change on the auth-key options or admin user table — exactly the correlation Logystera detects and alerts on early. The detection is not the result of running a malware scanner; it is the result of treating the three signals as a joined stream and watching for the shape.
You do not need a SIEM to do this, but you do need somewhere those three signal types arrive together, get joined on user and time window, and can fire an alert when the shape matches. That is the prevention layer that does not exist by default and is invisible to your users until the first thing they notice is a logout you didn't perform.
7. Related Silent Failures
- WordPress credential stuffing — auth.attempt brute force on /wp-login.php — the noisy precursor to the quiet hijack.
- WordPress REST API user enumeration — auth.attempt against /wp-json/wp/v2/users — how attackers find admin usernames in the first place.
- WordPress role escalation — wp.state_change on user_capabilities — the persistence step after a session hijack.
- WordPress plugin silent activation — wp.state_change on active_plugins — the implant step that turns a session into a backdoor.
- WordPress XML-RPC abuse — auth.attempt floods on /xmlrpc.php — alternate auth surface attackers use when /wp-login.php is rate-limited.
See what's actually happening in your WordPress system
Connect your site. Logystera starts monitoring within minutes.