This is a front-desk badge printer that will issue a master key if any paying guest presses the right button
CVE-2026-6898 is a missing capability check in WishListMember3_Hooks::generate_api_key in WishList Member for WordPress. Affected versions are all releases up to and including 3.30.1; the fix is reported by Patchstack as 3.31.0. The vulnerable path lets an authenticated low-privilege user hit the wlm3_generate_api_key AJAX action, replace the plugin's REST API secret, and then use that new secret to create an administrator-capable membership level and an arbitrary admin user.
The vendor-style 8.8 HIGH score gets the *impact* right but overstates the *reach*. This is not unauthenticated remote code execution against every public WordPress site; it needs a valid account first and a commercial plugin with a relatively small exposed footprint. But because this is a membership plugin, the Subscriber prerequisite is materially weaker than it sounds in many real deployments, so this stays HIGH instead of dropping to medium.
4 steps from start to impact.
Land a Subscriber account
admin-ajax.php. On WishList Member deployments, that often means self-registration, cheap trial access, reused credentials, or a previously compromised customer account rather than a full initial breach. Tooling is pedestrian: Burp Suite, curl, or credential-stuffing kits are enough.- WishList Member is installed and vulnerable (
<= 3.30.1) - Attacker has valid WordPress credentials at Subscriber level or above
- The site exposes a member login or registration path
- No unauthenticated path
- Closed-membership sites with manual approval block the easiest route
- MFA or anti-automation on login raises cost
Call wlm3_generate_api_key
/wp-admin/admin-ajax.php, the attacker invokes the vulnerable AJAX action that ultimately reaches WishListMember3_Hooks::generate_api_key. Because the function lacks a capability check, the application treats a Subscriber like an administrator for this operation. The weaponized tools here are simple HTTP clients, not exploit frameworks.- Authenticated session or valid cookies/nonces if required by the action wiring
- Endpoint reachable at
/wp-admin/admin-ajax.php
- A WAF or custom rule targeting the specific AJAX action can stop this cold
- Some sites may layer extra access control around admin AJAX
- Nonce handling can slow blind automation even if it does not fix the bug
action=wlm3_generate_api_key. Native WordPress logs usually do not give enough semantic detail unless request logging is already enabled.Rotate the REST API secret
- The AJAX action successfully updates or exposes the API secret
- WishList Member REST API functionality is enabled as normal
- If API access is disabled or heavily filtered, the chain may stop here
- Operators who rotate the secret quickly can invalidate attacker follow-on requests
Create an admin-mapped level and user
- Attacker can reach the WishList Member API endpoints
- API operations permit level creation and user registration
- No secondary control blocks role assignment changes
- Change monitoring on roles, plugins, or newly created admins can contain blast radius
- Some hardened sites disable plugin/theme editing from the dashboard
administrator, and plugin/theme installs shortly after member-logins./wp-admin/admin-ajax.php when action=wlm3_generate_api_key. This is the cleanest temporary choke point and should be in place within 30 days if you cannot patch immediately.administrator users, membership-level changes mapped to admin, and API key regeneration events. This will not prevent exploitation, but it gives you the best chance of catching follow-on abuse within 30 days while patch rollout finishes./wp-admin/admin-ajax.php. Apply within 30 days on internet-facing membership sites that cannot be patched quickly.- Relying on nonce checks alone does not solve a missing capability check; a valid low-privilege session can still be unauthorized for the action.
- Generic EDR on the web server will not stop the initial privilege escalation because the exploit is an application-layer logic bug, not malware execution at first touch.
- Blocking
/wp-admin/broadly does not necessarily help if legitimate member workflows already exposeadmin-ajax.php; many WordPress sites intentionally leave that path reachable.
The supporting signals.
| In-the-wild status | No CISA KEV listing found, and I found no primary-source public exploitation bulletin for this exact CVE. |
|---|---|
| Proof-of-concept availability | I found no exact public PoC for CVE-2026-6898; however, the exploit path is low-complexity and Patchstack/NVD describe the vulnerable AJAX action clearly enough that reproduction is straightforward. |
| EPSS | 0.00044 from the user-provided intel block, which is very low and consistent with a niche product plus authenticated-only reach. |
| KEV status | Not listed in the CISA Known Exploited Vulnerabilities Catalog. |
| CVSS vector | CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H — the crucial term is PR:L. On paper that is a major brake; on a membership plugin it is less comforting because low-privilege users are part of the product design. |
| Affected versions | Authoritative CVE/NVD text says all versions up to and including 3.30.1 are affected. |
| Fixed version | Patchstack reports 3.31.0 as the patched version. Vendor release notes for 3.31.0 list security fixes, though they do not name this CVE explicitly. |
| Exposure footprint | WebTechSurvey currently reports roughly 468 observed domains using WishList Member X, with 280 in the U.S. That is not tiny, but it is far from a mass-market WordPress plugin. |
| Disclosure timeline | NVD published the CVE on 2026-05-23; Patchstack published its advisory on 2026-05-25. |
| Researcher / source | NVD lists Wordfence as the source CNA. Patchstack credits researcher h0xilo on its advisory page. |
Sources.
Why this verdict
- Downgrade from 8.8:
PR:Lmatters in the real world; the attacker needs a working account first, which is a meaningful prerequisite versus unauthenticated WordPress bugs. - Not a full downgrade to medium: this is a *membership* plugin, so Subscriber accounts are often normal business objects, not rare internal users. That weakens the usual comfort defenders take from
PR:L. - Blast radius is total on a hit: once the REST API secret is rotated, the attacker can create an admin-capable membership level and arbitrary admin users, which is effectively site takeover.
- Exposure is narrower than commodity WordPress bugs: observed public footprint is in the hundreds of domains, not hundreds of thousands, which caps campaign value and keeps the score below the vendor baseline.
- Threat intel is quiet: no KEV entry, no exact public PoC located, and a very low EPSS all argue against pushing this into the top emergency tier today.
Why not higher?
This is not unauthenticated and not broadly applicable across the WordPress ecosystem; it only matters where WishList Member is installed and vulnerable. I also found no hard evidence of active exploitation or an exact public PoC for this CVE, which removes the biggest reason to score it as near-critical.
Why not lower?
On affected membership sites, the authentication requirement is often weak in practice because obtaining or creating a low-tier member account may be cheap, automated, or entirely expected. And once the bug is reached, the chain does not stop at data tampering; it ends in administrator creation and durable control of the site.
Crowdsourced verification payload.
Run this on the target WordPress host or a mounted web root from an auditor workstation. Invoke it as bash check_wlm_cve_2026_6898.sh /var/www/html and use a user that can read the WordPress files; root is not required unless filesystem permissions are tight.
#!/usr/bin/env bash
# check_wlm_cve_2026_6898.sh
# Detects whether WishList Member for WordPress is vulnerable to CVE-2026-6898
# Output: VULNERABLE / PATCHED / UNKNOWN
# Exit codes: 1=vulnerable, 0=patched, 2=unknown
set -u
SAFE_VERSION="3.31.0"
WP_PATH="${1:-.}"
PLUGIN_SLUG="wishlist-member-x"
PLUGIN_DIR="$WP_PATH/wp-content/plugins/$PLUGIN_SLUG"
verlte() {
[ "$1" = "$2" ] && return 0
[ "$(printf '%s\n%s\n' "$1" "$2" | sort -V | head -n1)" = "$1" ]
}
extract_version_from_file() {
local f="$1"
[ -f "$f" ] || return 1
awk -F': *' '/^[[:space:]]*Version:[[:space:]]*/ {print $2; exit}' "$f" | tr -d '\r'
}
find_version() {
local v=""
if command -v wp >/dev/null 2>&1; then
v=$(wp --path="$WP_PATH" plugin get "$PLUGIN_SLUG" --field=version 2>/dev/null | tr -d '\r')
if [ -n "$v" ]; then
printf '%s' "$v"
return 0
fi
fi
if [ ! -d "$PLUGIN_DIR" ]; then
return 1
fi
for candidate in \
"$PLUGIN_DIR/wishlist-member-x.php" \
"$PLUGIN_DIR/wishlist-member.php" \
"$PLUGIN_DIR/plugin.php" \
"$PLUGIN_DIR/wlm.php" \
"$PLUGIN_DIR/loader.php"
do
v=$(extract_version_from_file "$candidate")
if [ -n "$v" ]; then
printf '%s' "$v"
return 0
fi
done
v=$(grep -R -m1 -h -E '^[[:space:]]*Version:[[:space:]]*[0-9]+' "$PLUGIN_DIR" 2>/dev/null | head -n1 | sed -E 's/^[[:space:]]*Version:[[:space:]]*//; s/\r$//')
if [ -n "$v" ]; then
printf '%s' "$v"
return 0
fi
return 1
}
VERSION="$(find_version)"
STATUS=$?
if [ $STATUS -ne 0 ] || [ -z "$VERSION" ]; then
echo "UNKNOWN: Could not determine WishList Member version at $WP_PATH"
exit 2
fi
if verlte "$VERSION" "3.30.1"; then
echo "VULNERABLE: WishList Member version $VERSION is <= 3.30.1 (fixed in $SAFE_VERSION or later)"
exit 1
fi
if verlte "$SAFE_VERSION" "$VERSION"; then
echo "PATCHED: WishList Member version $VERSION is >= $SAFE_VERSION"
exit 0
fi
echo "UNKNOWN: Detected version $VERSION but could not classify confidently"
exit 2