CVE case study

CVE-2026-16613: A Cross-Site Request Could Expire WordPress Session Cookies

GDPR Cookie Compliance exposed a cookie-clearing branch without a request-origin check and used a case-sensitive WordPress allowlist. Version 5.1.0 requires same-origin POST and protects WordPress cookies case-insensitively.

Severity
Medium (4.3)
Scoring
CVSS 3.1
Weakness
CWE-352
Affected
GDPR Cookie Compliance versions earlier than 5.1.0
Remediation state
Upgrade to GDPR Cookie Compliance 5.1.0 or later
Advisory published
27 Jul 2026

Official vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:L

Why it matters

A consent tool may legitimately clear first-party cookies when a visitor withdraws consent. That operation changes browser state and needs the same request-origin protection as any other state-changing action.

The plugin registered moove_gdpr_get_scripts for unauthenticated AJAX use. Its nonce call did not validate request data, and the branch reached when strict was absent iterated through cookies sent by the browser and expired them.

WordPress authentication cookie names are lowercase, while the affected allowlist searched for uppercase WordPress. A top-level cross-site GET could carry a default Lax session cookie and receive expiration headers, forcing the user to sign in again.

How I found it

On 21 July 2026, I reviewed unauthenticated AJAX actions in GDPR Cookie Compliance and focused on operations that changed browser state. moove_gdpr_get_scripts could enter its cookie-clear branch without a valid nonce or an origin decision.

I followed the loop over the browser's Cookie header and noticed that the WordPress exclusion used a case-sensitive uppercase string. Normal authentication cookies use lowercase wordpress_, so the check did not protect them.

A disposable local test recorded expiration headers for synthetic WordPress and application cookies on the affected release. After the 5.1.0 repair, the same cross-site or GET condition produced no deletion headers.

WPScan withholds its complete operational PoC until 10 August 2026. This page therefore documents the public source boundary and a non-network policy model, not an executable request.

Root cause

wp_verify_nonce() was called with literal values and its result was discarded. It therefore provided neither a caller-bound token check nor a rejection path.

The handler did not require POST or same-origin navigation before entering its cookie-deletion branch. It trusted ambient browser cookies even when the request itself came from outside the site.

The allowlist used strpos($name, 'WordPress'), a case-sensitive comparison that did not match normal wordpress_logged_in_* and wordpress_sec_* names.

Source-to-sink trace

  1. 01
    Public actionwp_ajax_nopriv registration for moove_gdpr_get_scripts

    The consent endpoint is available without a WordPress account because visitors need its script response.

  2. 02
    Missing request gatemoove_gdpr_get_scripts() before 5.1.0

    The nonce call did not validate caller input or drive a rejection, and the state-changing branch did not require same-origin POST.

  3. 03
    Ambient session sourceHTTP Cookie header

    A top-level navigation can carry a default Lax cookie, exposing the victim's own browser state to the deletion loop.

  4. 04
    Case-sensitive allowlist gapstrpos(cookie_name, 'WordPress')

    The uppercase comparison misses canonical lowercase WordPress authentication cookie names.

  5. 05
    Fixed boundarysame-origin POST gate and stripos() in 5.1.0

    The mutating branch now requires an explicit origin and method decision, and the platform cookie comparison is case-insensitive.

Safe proof of concept

Prerequisites

  • Public GDPR Cookie Compliance source from a version earlier than 5.1.0 and from 5.1.0.
  • Python 3 for a local list-and-policy model.
  • No logged-in WordPress session, public site, real cookie value, or cross-site page.

Step-by-step reproduction

  1. Compare the cookie-clearing controller between the affected and fixed versions. Locate the same-origin POST branch and the switch from strpos() to stripos().
  2. Run the synthetic model with invented cookie names and empty values. It models only which names the two policies would consider removable.
  3. Observe that the affected case-sensitive check includes the lowercase WordPress session name in its deletion set.
  4. Observe that the fixed policy refuses a cross-origin GET before evaluating cookies.
  5. Run the fixed same-origin POST control and confirm that it still excludes the WordPress session name while allowing an ordinary analytics marker to be cleared.

Compare the public cookie-deletion boundary

LAB_DIR=$(mktemp -d "\${TMPDIR:-/tmp}/gdpr-cookie-cve.XXXXXX")

curl -fsSL \
  https://plugins.svn.wordpress.org/gdpr-cookie-compliance/tags/5.0.17/controllers/class-moove-gdpr-controller.php \
  -o "$LAB_DIR/affected.php"
curl -fsSL \
  https://plugins.svn.wordpress.org/gdpr-cookie-compliance/tags/5.1.0/controllers/class-moove-gdpr-controller.php \
  -o "$LAB_DIR/fixed.php"

diff -u "$LAB_DIR/affected.php" "$LAB_DIR/fixed.php"

Synthetic cookie-policy model

cookie_names = [
    "wordpress_logged_in_SYNTHETIC",
    "wp-settings-time-1",
    "analytics_marker",
    "moove_gdpr_popup",
]

def affected_deletion_set(names):
    return [name for name in names
            if name != "moove_gdpr_popup" and "WordPress" not in name]

def fixed_deletion_set(names, method, same_origin):
    if method != "POST" or not same_origin:
        return []
    return [name for name in names
            if name != "moove_gdpr_popup" and "wordpress" not in name.lower()]

print("affected:", affected_deletion_set(cookie_names))
print("fixed cross-site GET:", fixed_deletion_set(cookie_names, "GET", False))
print("fixed same-origin POST:", fixed_deletion_set(cookie_names, "POST", True))

Expected evidence

  • The affected set includes the synthetic lowercase WordPress session name.
  • The fixed cross-site GET set is empty because the method and origin policy fails first.
  • The fixed same-origin POST set excludes the WordPress session and keeps only ordinary removable markers.
  • No actual cookie or HTTP response is created by the model.
Negative control

Use a same-origin POST with only the consent cookie and a synthetic analytics marker. The fixed policy should preserve the consent control and identify only the ordinary removable marker.

Fixed-version re-test

On version 5.1.0 or later, cross-origin and GET requests must not reach the deletion branch. A legitimate same-origin POST must preserve WordPress authentication cookies regardless of case.

Impact

After a user followed a crafted top-level link, an affected site could return Set-Cookie headers that expired its WordPress session and other non-allowlisted first-party cookies. The result was a recoverable forced logout and loss of browser state.

WPScan scores the issue Medium at 4.3 with Low availability impact only. It does not expose cookie values to the attacker, bypass authentication, or create a persistent denial of service.

Fix and retest

Upgrade to GDPR Cookie Compliance 5.1.0 or later. The repair rejects a cross-origin Referer and permits the mutating cookie-clear branch only for a same-origin POST.

The fixed code uses stripos() for WordPress cookie names, preventing case differences from removing them even during a legitimate consent update.

Retest with synthetic cookie names. Cross-origin and GET requests must not produce expiration headers; a valid same-origin POST may clear the intended analytics control while leaving WordPress authentication cookies untouched.

Engineering lesson

Cookie deletion is a security-sensitive state transition. Public AJAX registration does not remove the need for origin validation, method restrictions, and a deliberate allowlist.

Identifier comparisons should match the producer's real casing rules. A security allowlist that differs from the platform's canonical naming can silently exclude the values it was written to protect.

References

Follow this thread

Related research, source, and writing.

These paths share a published research boundary, project source, article series, or authorized lab context with this record.

Back to article start