A receptionist app that lets any logged-in customer flip the 'confirmed' switch on someone else's haircut
The Salon Booking System plugin exposes an AJAX action that skips a capability/ownership check. Any authenticated user at Subscriber level or above can invoke the action and manipulate booking state — approving, rejecting, or otherwise mutating appointment records that don't belong to them. All versions prior to 10.30.20 are affected; the free plugin has roughly 5k-10k active installs per wordpress.org telemetry, so the fleet-wide blast radius is small. The bug class is a classic *missing_function_level_access_control* (CWE-862) inside admin-ajax.php handlers — the plugin trusts the nonce but never re-checks current_user_can() against the target booking's owner.
There is no vendor CVSS because MITRE/NVD have not scored it and WPScan's provisional rating for the sibling entry sits at ~4.3 medium. Reality matches that: the attacker must already hold a WordPress account on the target site (registration is off by default on most WP installs), the affected object class is *booking metadata* — not credentials, files, or code paths — and the plugin's install base is measured in thousands, not millions. LOW is the honest call.
4 steps from start to impact.
Obtain a Subscriber-level account on the target site
users_can_register is enabled) or leverages a stolen/purchased subscriber credential. Subscriber is the lowest authenticated role and grants nothing beyond a profile page — the bar to reach it is the WP registration form plus email confirmation.- Target has open registration OR attacker has valid low-tier credentials
- Salon Booking System <10.30.20 installed and active
- Most production WP sites disable open registration
- Sites using Cloudflare Turnstile / hCaptcha on signup block bulk enumeration
Enumerate booking IDs
/wp-json/ or the booking calendar frontend to harvest integer booking IDs. IDs are auto-increment and predictable, so a simple range sweep enumerates the entire salon calendar.- Booking IDs are integer and sequential
- No WAF rate limiting on authenticated requests
- Rate-limit rules on
admin-ajax.phpfrom Wordfence Premium blunt enumeration - Sites behind Cloudflare bot-management flag automated crawls
admin-ajax.php POST volume from a single session ID is a stock Wordfence anomaly ruleInvoke the vulnerable AJAX action with forged booking_id
wp-admin/admin-ajax.php?action=<vulnerable_action> with the nonce from their own logged-in session and a booking_id belonging to another customer. The handler processes the mutation without verifying that the current user owns the booking or holds manage_options.- Valid nonce from own session
- Knowledge of target booking_id
- Impact is bounded to the booking object schema — no file write, no SQL escape, no code execution
- The salon owner still sees the tampered state in the admin dashboard
Impact: appointment tampering, denial-of-service to salon operations
- Successful step 3
- No pivot to WP core admin
- No path to code execution or credential theft
The supporting signals.
| In-the-wild exploitation | None observed. No GreyNoise tag, no Wordfence attack-telemetry campaign as of 2026-07-01 |
|---|---|
| Public PoC | No public PoC repo. The WPScan advisory describes the flaw class but withholds the exact action name pending patch uptake |
| EPSS | Not yet scored (typical lag of 7-14 days for newly minted CVEs); expected <0.5% |
| KEV status | Not listed. CISA KEV threshold requires observed exploitation |
| CVSS vector | No official vector. Reasonable estimate: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N ≈ 5.4 medium; noisgate reassesses to 3.5 LOW given install base |
| Affected versions | Salon Booking System < 10.30.20 (free plugin); Pro variant likely shares the code path — verify against vendor changelog |
| Fixed version | 10.30.20 (released ~June 2026) |
| Install base / exposure | wordpress.org reports ~5k–10k active installs; Shodan/Censys do not fingerprint this plugin path meaningfully |
| Disclosure date | 2026-07-01 (CVE publication); vendor fix landed earlier in June |
| Reporter | Attributed via WPScan / Wordfence threat-intel programs (individual researcher name not published) |
noisgate verdict.
The single decisive factor is role-and-scope containment: exploitation requires a live Subscriber account on the affected WordPress site AND the blast radius is limited to booking-object metadata inside a plugin with a ~5k–10k install footprint. There is no path to WP core takeover, code execution, or fleet-scale compromise.
Why this verdict
- Auth required, lowest tier: the vulnerable AJAX handler is only reachable by an authenticated Subscriber. Most production WP installs disable open registration; on those sites the exploit population collapses to insiders and credential-stuffing survivors.
- Blast radius is booking metadata: no code execution, no privilege escalation to Editor/Admin, no filesystem write, no credential disclosure beyond booking-form PII. This is a business-logic authz bug, not a RCE.
- Install base is small: ~5k–10k active installs per wordpress.org. Even a 100% exploitation rate does not move the needle for a 10k-host enterprise fleet unless the org runs a salon-industry SaaS.
- Role multiplier — low-value role only: Salon Booking runs on marketing/customer-portal WordPress sites (role class a-b in the noisgate spectrum). It is never deployed on identity, PKI, hypervisor, backup, CI/CD, or edge appliances. The high-value-role floor does not apply — the component cannot be a domain controller no matter how you deploy it.
- No KEV, no PoC, no active exploitation: none of the amplifiers that would override friction are present as of 2026-07-01.
Why not higher?
MEDIUM would require either broader install exposure, unauthenticated reachability, or impact that touches WP core or customer credentials. None apply — the bug is gated by Subscriber auth and impacts only plugin-scoped booking objects. Without KEV listing or a public weaponized PoC there is no in-the-wild pressure to raise the bucket.
Why not lower?
IGNORE is inappropriate because the flaw does have a real integrity impact on booking data — for a salon that IS the business — and PII (name/phone/email) is readable inside the booking record. Organizations subject to GDPR/CCPA still owe a remediation obligation, so backlog-hygiene tracking is warranted.
What to do — in priority order.
- Disable open user registration on the WordPress site — Settings → General → uncheck *Anyone can register*. Kills the primary exploit path where an attacker self-provisions a Subscriber account. No mitigation SLA at LOW — apply opportunistically, no clock.
- Restrict
admin-ajax.phpat the WAF for the vulnerable action name — Wordfence / Cloudflare custom rule blocking POSTs toadmin-ajax.phpwhereactionmatches the plugin handler and the session role is Subscriber. Buys time before you can schedule the plugin update. - Update Salon Booking System to 10.30.20 or later — This is the definitive fix. Because verdict is LOW there is no mitigation SLA — go straight to the noisgate remediation SLA of ≤365 days; realistically fold into the next monthly plugin-maintenance window.
- Audit existing Subscriber accounts —
wp user list --role=subscriber --format=csv— review for accounts you don't recognize, especially those created after the disclosure date. Delete stale accounts. - Enable plugin auto-updates for low-risk maintenance CVEs — For this class of niche plugin, auto-update via WP dashboard or WP-CLI cron eliminates the need for manual tracking of every 3.5-rated advisory.
- MFA on WP admin — does not help because the exploit path only needs Subscriber, and Subscribers frequently don't have MFA enforced.
- IP allowlisting
/wp-admin/— the vulnerable endpoint is/wp-admin/admin-ajax.php, which most sites keep open for frontend AJAX (cart, forms, search). Blocking it breaks the site. - EDR on the web server — this is a logic bug inside PHP handler code. No process spawns, no file writes, no signal for endpoint agents to catch.
Crowdsourced verification payload.
Run on the WordPress host (or any host with WP-CLI pointed at the install). Requires shell access and the wp binary. Example: ./check-cve-2026-11887.sh /var/www/html.
#!/usr/bin/env bash
# noisgate verifier — CVE-2026-11887
# Salon Booking System AJAX authz bypass; fixed in 10.30.20
# Usage: ./check-cve-2026-11887.sh <wp_docroot>
set -u
WP_ROOT="${1:-}"
FIXED="10.30.20"
SLUG="salon-booking-system"
if [[ -z "$WP_ROOT" || ! -d "$WP_ROOT" ]]; then
echo "UNKNOWN: wp_docroot not provided or not a directory" >&2
exit 2
fi
if ! command -v wp >/dev/null 2>&1; then
echo "UNKNOWN: wp-cli not installed" >&2
exit 2
fi
# Is the plugin present?
if ! wp --path="$WP_ROOT" --allow-root plugin is-installed "$SLUG" 2>/dev/null; then
echo "PATCHED: plugin $SLUG not installed"
exit 0
fi
STATUS=$(wp --path="$WP_ROOT" --allow-root plugin get "$SLUG" --field=status 2>/dev/null)
VER=$(wp --path="$WP_ROOT" --allow-root plugin get "$SLUG" --field=version 2>/dev/null)
if [[ -z "$VER" ]]; then
echo "UNKNOWN: could not read plugin version" >&2
exit 2
fi
# semver compare
ver_lt() {
[[ "$1" == "$2" ]] && return 1
local lower
lower=$(printf '%s\n%s\n' "$1" "$2" | sort -V | head -n1)
[[ "$lower" == "$1" ]]
}
if ver_lt "$VER" "$FIXED"; then
echo "VULNERABLE: $SLUG $VER < $FIXED (status=$STATUS)"
exit 1
else
echo "PATCHED: $SLUG $VER >= $FIXED (status=$STATUS)"
exit 0
fi
If you remember one thing.
Sources
What defenders are saying.
Crowdsourced verification outputs.
Results submitted by users who ran the verification payload against their environment.