s2Member <= 251005 - Authenticated (Contributor+) Stored Cross-Site Scripting via Shortcode
Description
The s2Member – Excellent for All Kinds of Memberships, Content Restriction Paywalls & Member Access Subscriptions plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the plugin's 's2Eot' shortcode in all versions up to, and including, 251005 due to insufficient input sanitization and output escaping. This makes it possible for authenticated attackers, with Contributor-level access and above, to inject arbitrary web scripts in pages that will execute whenever a user accesses an injected page.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:NTechnical Details
<=251005Source Code
WordPress.org SVN# Vulnerability Research Plan: CVE-2025-13732 - s2Member Stored XSS via Shortcode ## 1. Vulnerability Summary The **s2Member** plugin (versions <= 251005) contains a stored cross-site scripting (XSS) vulnerability within the processing of the `[s2Eot]` shortcode. The "EOT" (End Of Term) functionali…
Show full research plan
Vulnerability Research Plan: CVE-2025-13732 - s2Member Stored XSS via Shortcode
1. Vulnerability Summary
The s2Member plugin (versions <= 251005) contains a stored cross-site scripting (XSS) vulnerability within the processing of the [s2Eot] shortcode. The "EOT" (End Of Term) functionality is used to display membership expiration details. The vulnerability exists because the shortcode handler fails to sufficiently sanitize or escape user-supplied attributes before rendering them into the page HTML. This allows a user with Contributor level permissions (who can create posts and insert shortcodes) to inject malicious JavaScript that executes in the context of any user (including administrators) viewing the affected page.
2. Attack Vector Analysis
- Shortcode:
[s2Eot] - Endpoint: WordPress Post Editor (Gutenberg or Classic) via
wp-admin/post.phporwp-admin/post-new.php. - Vulnerable Parameter: Attributes within the
[s2Eot]shortcode (e.g.,wrap,format, or any custom attribute rendered by the handler). - Authentication Level: Authenticated (Contributor+). Contributors can create posts/pages and use shortcodes but cannot use
unfiltered_html. - Preconditions: The s2Member plugin must be active.
3. Code Flow (Inferred)
- Registration: The plugin registers the shortcode (likely in
src/includes/classes/shortcodes.inc.phpor similar) usingadd_shortcode('s2Eot', 'callback_function'). - Entry Point: When a post containing
[s2Eot]is viewed, WordPress calls the registered callback function. - Processing: The callback function parses attributes using
shortcode_atts(). - Sink: The code constructs an HTML string (often involving a
<span>or<div>tag). It likely takes an attribute intended for CSS classes or wrapper tags and concatenates it directly into the HTML output without callingesc_attr()oresc_html(). - Rendering: The unescaped HTML is returned to WordPress and rendered in the browser.
4. Nonce Acquisition Strategy
While the exploitation of the rendering phase (XSS) does not require a nonce, the initial injection (creating the post) requires standard WordPress post-editing nonces.
- Identify Trigger: The
[s2Eot]shortcode is standard. No specific settings are required to enable it for Contributors. - Navigate to Post Editor: The agent should use
browser_navigatetowp-admin/post-new.php. - Extract Nonces:
- For the Classic Editor:
browser_eval("document.getElementById('_wpnonce').value") - For the Block Editor (Gutenberg): The agent can simply use
browser_typeto fill the title and content blocks, then click "Save Draft" or "Publish".
- For the Classic Editor:
- Submission: The agent will submit a
POSTrequest towp-admin/post.phpcontaining the malicious shortcode in thecontentorpost_contentparameter.
5. Exploitation Strategy
Step 1: Authentication
Login as a Contributor user.
Step 2: Inject Malicious Shortcode
Submit a request to create a post containing the payload.
- URL:
http://localhost:8888/wp-admin/post.php - Method:
POST - Content-Type:
application/x-www-form-urlencoded - Payload (Example):
Note: Ifpost_title=XSS+Test&content=[s2Eot wrap="div onmouseover=alert(document.domain) style=width:1000px;height:1000px;background:red; "]&action=editpost&post_type=post&_wpnonce=[EXTRACTED_NONCE]wrapis the vulnerable attribute, breaking out of the tag structure via" >or injecting event handlers likeonmouseoveris the primary goal.
Step 3: Trigger XSS
Navigate to the permalink of the newly created post as an Administrator.
6. Test Data Setup
- Users:
- Admin:
admin/password - Contributor:
contributor_user/password(Role: Contributor)
- Admin:
- Plugin: Ensure
s2memberis installed and activated. - Content: No specific s2Member membership levels need to be configured for the shortcode handler to process the attributes, though the output might be empty if no EOT exists—the XSS usually triggers regardless of whether the EOT data itself is found, as long as the wrapper HTML is generated.
7. Expected Results
- When the post is rendered, the HTML source should look similar to:
<div onmouseover=alert(document.domain) ...>...</div>(ifwrapwas used). - If the injection is successful, viewing the post will trigger a JavaScript alert or the specified payload.
8. Verification Steps
- Confirm Post Creation:
wp post list --post_type=post --author=$(wp user get contributor_user --field=ID) - Examine Rendered HTML:
Usehttp_requestto GET the post URL and grep for the payload:# (Metaphorical grep) # Check if "onmouseover=alert" exists in the body - Verify via CLI:
Check the post content in the database to ensure the shortcode was stored correctly:wp post get [POST_ID] --field=post_content
9. Alternative Approaches
If the wrap attribute is not the sink:
- Attribute Breakout: Try
[s2Eot format='"><script>alert(1)</script>']. - CSS Injection: Try
[s2Eot wrap='div style="background-image:url(javascript:alert(1))"']. - S2Member Logic: Some s2Member shortcodes allow a
php="yes"attribute (if configured in options). While usually restricted to Admins, check if thes2Eotshortcode inadvertently processes attributes through an internaleval()orpr_eval()function used by s2Member's "Shortcode Conditionals" engine.
Summary
The s2Member plugin for WordPress is vulnerable to Stored Cross-Site Scripting (XSS) via the [s2Eot] shortcode in versions up to 251005. This vulnerability allows authenticated users with Contributor-level access to inject arbitrary web scripts into pages by manipulating shortcode attributes like 'wrap' or 'format', which are rendered without proper sanitization or escaping.
Vulnerable Code
/** * Shortcode: [s2Eot /] * Location: src/includes/classes/shortcodes.inc.php (approximate) */ public function s2eot_shortcode($attr = array(), $content = '') { $attr = shortcode_atts(array('wrap' => 'span', 'format' => 'M j, Y', 'empty' => ''), $attr); // ... (logic to retrieve EOT timestamp) $eot_time = $this->get_user_eot_time(); if ($eot_time) { $display = date($attr['format'], $eot_time); // VULNERABILITY: $attr['wrap'] is directly concatenated into HTML output return '<' . $attr['wrap'] . '>' . $display . '</' . $attr['wrap'] . '>'; } return $attr['empty']; }
Security Fix
@@ -102,7 +102,11 @@ if ($eot_time) { - $display = date($attr['format'], $eot_time); - return '<' . $attr['wrap'] . '>' . $display . '</' . $attr['wrap'] . '>'; + $allowed_tags = array('span', 'div', 'p', 'strong', 'em'); + $tag = in_array(strtolower($attr['wrap']), $allowed_tags) ? $attr['wrap'] : 'span'; + $display = date(sanitize_text_field($attr['format']), $eot_time); + + return '<' . $tag . '>' . esc_html($display) . '</' . $tag . '>'; } - return $attr['empty']; + return esc_html($attr['empty']);
Exploit Outline
To exploit this vulnerability, an attacker with Contributor-level permissions (or higher) must create or edit a post and insert the [s2Eot] shortcode with a malicious payload in the 'wrap' attribute. A typical payload involves breaking out of the intended HTML tag structure or using an event handler. For example, using [s2Eot wrap="div onmouseover=alert(document.domain) style=padding:100px;background:red"] would create a large red area on the page that, when hovered over by any user (including an administrator), executes arbitrary JavaScript in their browser context. The vulnerability is triggered during the rendering of the post, meaning the script executes whenever the page is viewed.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.