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
- 01Public action
wp_ajax_nopriv registration for moove_gdpr_get_scriptsThe consent endpoint is available without a WordPress account because visitors need its script response.
- 02Missing request gate
moove_gdpr_get_scripts() before 5.1.0The nonce call did not validate caller input or drive a rejection, and the state-changing branch did not require same-origin POST.
- 03Ambient session source
HTTP Cookie headerA top-level navigation can carry a default Lax cookie, exposing the victim's own browser state to the deletion loop.
- 04Case-sensitive allowlist gap
strpos(cookie_name, 'WordPress')The uppercase comparison misses canonical lowercase WordPress authentication cookie names.
- 05Fixed boundary
same-origin POST gate and stripos() in 5.1.0The 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
- Compare the cookie-clearing controller between the affected and fixed versions. Locate the same-origin POST branch and the switch from
strpos()tostripos(). - Run the synthetic model with invented cookie names and empty values. It models only which names the two policies would consider removable.
- Observe that the affected case-sensitive check includes the lowercase WordPress session name in its deletion set.
- Observe that the fixed policy refuses a cross-origin GET before evaluating cookies.
- 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.
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.
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.