CVE-2026-11855: Forged Stripe Webhook Metadata to Admin-Context XSS in Simple Membership
Simple Membership 4.7.4 accepted unauthenticated forged Stripe webhook metadata when the signing secret was empty, then rendered the stored API version in an administrator notice without safe output encoding.
CVE-2026-11855: Forged Stripe Webhook Metadata to Admin-Context XSS in Simple Membership
Overview
Simple Membership 4.7.4 contained an unauthenticated stored cross-site scripting vulnerability in its Stripe webhook handling flow.
The bug started with a trust decision that looked small: the plugin stored metadata from a Stripe webhook payload. In the default installation state tested for this report, the Stripe webhook signing secret was empty. That meant the webhook endpoint accepted a forged unauthenticated request and processed attacker-controlled webhook metadata without first proving that the request really came from Stripe.
The second half of the issue happened later, in a different context. The stored api_version value was rendered inside an administrator dashboard notice without safe output encoding. A value that entered through an unauthenticated webhook request could therefore become active HTML/JavaScript when an administrator loaded /wp-admin/.
The vulnerability is tracked as CVE-2026-11855 and WPVDB ID 217cb606-a0f2-4427-9262-cfe1cc90474e. The validated version was Simple Membership 4.7.4; public advisory metadata describes the affected range as Simple Membership before 4.7.5.
The Vulnerable Workflow
The relevant feature was the plugin’s Stripe subscription webhook endpoint:
1
POST /?swpm_process_stripe_subscription=1&hook=1
A Stripe webhook handler normally sits on an important trust boundary. It receives requests from the public internet, but the application is expected to trust the payload only after verifying the webhook signature with a configured signing secret.
In the tested default state, the signing secret was not generated during installation and remained empty after plugin activation. With no effective authenticity check in place, an unauthenticated request could reach the webhook processing logic.
The value that mattered was not a payment amount or a subscription identifier. It was the webhook metadata field:
1
"api_version": "..."
The plugin stored this value in the WordPress option:
1
swpm_stripe_received_api_old_version
That option was later used to display an outdated Stripe API version notice in the WordPress administrator area.
Where the Trust Boundary Broke
The vulnerable path crossed two separate trust boundaries.
First, the webhook endpoint treated request-supplied metadata as if it came from Stripe. That trust should only exist after signature verification. In the tested configuration, the signing secret was empty, and the forged request was accepted.
Second, the stored metadata moved from a backend option into an administrator-facing notice. At that point, the value should have been treated as untrusted stored data and encoded for HTML output. Instead, the attacker-controlled value was rendered into the dashboard notice in a way that allowed script execution.
The resulting chain was:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Unauthenticated request
|
v
Forged Stripe webhook body
|
v
Unverified api_version metadata
|
v
Stored WordPress option
|
v
Admin dashboard notice
|
v
JavaScript execution in administrator context
This is what made the issue more than a simple missing escaping bug. The sink was an admin notice, but the source was an unauthenticated public webhook request.
How I Validated It
I tested the issue in a local WordPress lab with Simple Membership 4.7.4.
The important precondition was the default signing-secret state. After activating the plugin, the Stripe webhook signing secret was empty. I did not configure a secret before testing the webhook endpoint.
From there, the validation was straightforward:
- send a forged unauthenticated webhook request;
- confirm the request returned
HTTP 200 OK; - confirm that
swpm_stripe_received_api_old_versionwas updated; - load
/wp-admin/as an administrator; - confirm that the stored value was rendered into the dashboard notice as live DOM rather than inert text.
The key observation was that no administrator interaction was required beyond loading the dashboard. The stored payload was rendered automatically when the outdated Stripe API version notice appeared.
Proof of Concept
The following request shows the shape of the unauthenticated webhook request used during validation. The active script payload is omitted from this public write-up; the important security property is that the api_version value is attacker-controlled, stored, and later rendered in administrator context.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /?swpm_process_stripe_subscription=1&hook=1 HTTP/1.1
Host: example.test
Content-Type: application/json
{
"id": "evt_test",
"type": "omni.unused_event",
"created": 1780000000,
"api_version": "2020-01-01.<attacker-controlled-html>",
"data": {
"object": {
"object": "unknown"
}
}
}
The endpoint returned:
1
HTTP 200 OK
After the request, the WordPress option below contained the attacker-controlled API version value:
1
swpm_stripe_received_api_old_version
When an administrator loaded:
1
/wp-admin/
the outdated Stripe API version notice was displayed automatically. In the tested scenario, the injected HTML was parsed into the live browser DOM and JavaScript executed in the administrator session.
Why the Existing Checks Were Not Enough
There were two different defensive responsibilities in this path, and both mattered.
The webhook handler needed to answer:
1
Did this request genuinely come from Stripe?
That requires a valid signature check using a configured webhook signing secret. In the tested installation state, the secret was empty, so the endpoint accepted a forged request.
The administrator notice needed to answer a different question:
1
Is this stored value safe to render as HTML?
The answer should have been no. Even if a value is stored in a WordPress option, it is not automatically trusted. Storage changes the timing of the attack; it does not change the trust level of the data.
The safer model is:
1
2
3
4
verified webhook metadata
-> stored as data
-> encoded on output
-> rendered as text, not executable markup
Impact
The demonstrated impact was stored JavaScript execution in administrator context.
Because the payload executed when an administrator loaded the WordPress dashboard, an attacker could potentially perform actions available to that administrator’s browser session, including stealing administrator nonces or triggering privileged administrative requests.
This impact depends on the administrator visiting the dashboard after the malicious value has been stored. The supplied evidence does not establish remote code execution on the server or direct authentication bypass into the administrator account.
Remediation
The vulnerable design had two parts, so the fix should address both sides of the chain.
The webhook endpoint should reject unauthenticated or unsigned webhook requests. A Stripe webhook payload should only be processed after the signature is verified against a non-empty signing secret.
The administrator notice should also treat stored values as untrusted and encode them before rendering. Even verified metadata should be displayed as text unless the code has a very specific reason to allow HTML.
A safe version of the data flow should look like this:
1
2
3
4
5
Incoming webhook
-> verify Stripe signature with configured secret
-> reject if verification fails
-> store metadata as data
-> escape output in admin notice
Site owners should update Simple Membership to 4.7.5 or later and verify that Stripe webhook signing is configured correctly.
Recognizing This Pattern Elsewhere
This bug shape is common in webhook integrations:
1
2
3
4
5
6
7
8
9
Public webhook endpoint
+
Missing or weak authenticity verification
+
Stored provider metadata
+
Admin-facing rendering without escaping
=
Stored XSS from an unauthenticated source
When auditing similar code, I look for values such as:
1
2
3
4
5
6
7
api_version
event_type
customer_email
plan_name
product_name
failure_reason
provider_message
flowing from a webhook body into:
1
2
3
4
5
6
WordPress options
admin notices
settings pages
logs shown in wp-admin
email templates
HTML dashboards
The audit question is not only whether the webhook changes payment state. Metadata can be just as dangerous when it is later rendered to an administrator.
Technical Summary
1
2
3
4
5
6
7
8
9
10
11
12
CVE: CVE-2026-11855
Product: Simple Membership WordPress Plugin
Validated version: 4.7.4
Public affected range: < 4.7.5
Vulnerability class: Stored Cross-Site Scripting
CWE: CWE-79
Required privileges: Unauthenticated
Attack surface: Stripe webhook endpoint
Untrusted source: api_version field from forged webhook body
Stored location: swpm_stripe_received_api_old_version
Sink: WordPress administrator notice
Impact: JavaScript execution in administrator context
Research Credit
- Researcher: Duy Tran
- Disclosure: Responsible disclosure through the WordPress vulnerability ecosystem
- Coordinator / advisory source: WPScan / WPVDB
References
- CVE-2026-11855 — CVE.org
- CVE-2026-11855 — NVD / NIST
- WPScan / WPVDB advisory
- Simple Membership plugin page
- Stripe webhook signature verification documentation
Responsible Disclosure Notice
This write-up documents a vulnerability that has been publicly assigned a CVE and fixed in the public plugin release line.
Testing should be limited to systems you own or are explicitly authorized to assess. The proof of concept is included to explain the trust-boundary failure and support defensive validation, not to encourage testing against third-party WordPress sites.