CVE-2026-4025

PrivateContent Free <= 1.2.0 - Authenticated (Contributor+) Stored Cross-Site Scripting via 'align' Shortcode Attribute

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

Description

The PrivateContent Free plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the 'align' shortcode attribute in the [pc-login-form] shortcode in all versions up to, and including, 1.2.0. This is due to insufficient input sanitization and output escaping on the 'align' attribute. Specifically, the attribute value flows from the shortcode through pc_login_form() to pc_static::form_align(), where it is directly concatenated into an HTML class attribute without esc_attr() or any 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<=1.2.0
PublishedApril 7, 2026
Last updatedApril 8, 2026
Affected pluginprivatecontent-free

What Changed in the Fix

Changes introduced in v1.3.0

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Research Plan: CVE-2026-4025 - PrivateContent Free Stored XSS ## 1. Vulnerability Summary The **PrivateContent Free** plugin (up to 1.2.0) contains a stored cross-site scripting (XSS) vulnerability. The plugin fails to sanitize or escape the `align` attribute within the `[pc-login-form]` shortcod…

Show full research plan

Research Plan: CVE-2026-4025 - PrivateContent Free Stored XSS

1. Vulnerability Summary

The PrivateContent Free plugin (up to 1.2.0) contains a stored cross-site scripting (XSS) vulnerability. The plugin fails to sanitize or escape the align attribute within the [pc-login-form] shortcode (and likely other related shortcodes). This value is passed to pc_static::form_align() and concatenated directly into an HTML class attribute without using esc_attr(). Authenticated users with Contributor level access or higher can inject malicious JavaScript into pages, which will execute when any user (including administrators) views that page.

2. Attack Vector Analysis

  • Endpoint/Shortcode: [pc-login-form] (also check [pc-registration-form] and [pc-user-del-box]).
  • Vulnerable Parameter: The align attribute within the shortcode.
  • Authentication Level: Contributor+ (any role allowed to create or edit posts and use shortcodes).
  • Preconditions: The plugin must be active, and a post containing the malicious shortcode must be published or previewed.

3. Code Flow

Based on the vulnerability description and provided source files:

  1. Entry Point: A user with Contributor permissions creates a post with the shortcode [pc-login-form align='PAYLOAD'].
  2. Shortcode Handling: WordPress parses the shortcode and calls the handler function (inferred as pc_login_form() or the render_callback defined in Gutenberg integration like pvtcont_login_guten_handler).
  3. Data Processing: The align attribute value is extracted from the $atts array.
  4. Vulnerable Sink: The value is passed to pc_static::form_align().
  5. Output Generation: Inside pc_static::form_align(), the code likely performs a concatenation similar to:
    // Inferred vulnerable code in pc_static::form_align()
    return 'pc_form_' . $align_attr; 
    
    This returned string is then placed into a template:
    // Inferred output context
    echo '<div class="' . pc_static::form_align($atts['align']) . '">';
    
  6. Result: The lack of esc_attr() allows an attacker to break out of the class attribute and inject event handlers (e.g., onmouseover, autofocus onfocus).

4. Nonce Acquisition Strategy

Shortcodes in WordPress do not require nonces for rendering. They are processed on the server whenever a post is rendered for display. Therefore, no nonce is required for the core exploitation of this vulnerability.

However, if the PoC involves using the Gutenberg editor via the REST API to save the post, a standard WordPress REST API nonce (_wpnonce) would be required. This can be obtained by:

  1. Navigating to the post editor page (/wp-admin/post-new.php).
  2. Extracting the wpApiSettings.nonce from the page source using browser_eval.

5. Exploitation Strategy

The goal is to demonstrate that a Contributor user can inject a script that executes when viewed.

Step 1: Create a Post with Payload

Using the http_request tool, the agent will authenticate as a Contributor and create a post containing the malicious shortcode.

  • URL: http://localhost:8080/wp-admin/post-new.php (to initiate) or using wp-cli to create the post directly.
  • Shortcode Payload: [pc-login-form align=' " onmouseover="alert(document.domain)" style="padding:100px;border:1px solid red;" ']
    • Note: We use style to make the element large and easy to trigger the onmouseover event.

Step 2: View the Post

The agent will navigate to the newly created post's permalink using http_request.

Step 3: Analyze Response

Search the response body for the injected payload to confirm it rendered without escaping.

  • Target String: class="pc_login_form " onmouseover="alert(document.domain)"

6. Test Data Setup

  1. Plugin: Ensure privatecontent-free version 1.2.0 is installed and active.
  2. User: Create a user with the Contributor role.
    wp user create attacker attacker@example.com --role=contributor --user_pass=password123
    
  3. Post Content: Use the following content for the post:
    Check out this login form:
    [pc-login-form align=' " onmouseover="alert(document.domain)" ']
    

7. Expected Results

  • The HTTP response from the published page should contain the raw, unescaped payload within the class attribute of a div or form element.
  • Specifically, the HTML output should look like:
    <div class="pc_login_form " onmouseover="alert(document.domain)" ...
  • The " character in the align attribute will close the class attribute value, allowing the onmouseover attribute to be injected as a separate HTML attribute.

8. Verification Steps

  1. HTML Inspection: After the http_request to the post, use grep or string matching on the output:
    # Confirm the injection
    grep -P 'class="[^"]*"\s+onmouseover="alert'
    
  2. Database Check: Verify the shortcode was stored correctly via wp-cli:
    wp post get <POST_ID> --field=post_content
    

9. Alternative Approaches

If [pc-login-form] is patched or blocked, try the other Gutenberg-integrated elements found in the source:

  1. Registration Form: [pc-registration-form align=' " onmouseover="alert(1)" '] (linked to builders_integration/guten_elements/registr/registr.php)
  2. User Deletion Box: [pc-user-del-box align=' " onmouseover="alert(1)" '] (linked to builders_integration/guten_elements/user_del/user_del.php)

Both of these use the pc_align / align attribute pattern and are registered with similar render handlers that likely share the vulnerable pc_static::form_align() function.

Research Findings
Static analysis — not yet PoC-verified

Summary

The PrivateContent Free plugin for WordPress (up to version 1.2.0) is vulnerable to Stored Cross-Site Scripting due to insufficient input sanitization and output escaping on the 'align' attribute in shortcodes such as [pc-login-form]. An authenticated attacker with Contributor-level access or higher can inject arbitrary JavaScript by breaking out of the HTML class attribute context where the value is concatenated in pc_static::form_align().

Vulnerable Code

// builders_integration/guten_elements/login/login.php @ 1.2.0
'pc_align' => array(
	'label'		=> esc_html__('Form alignment', 'privatecontent-free'),
	'type'		=> 'select',
	'opts'		=> array(
		'center'	=> esc_html__('Center', 'privatecontent-free'),
		'left'		=> esc_html__('Left', 'privatecontent-free'),
		'right'		=> esc_html__('Right', 'privatecontent-free'),
	),
	'default' 	=> 'center',
	'panel'		=> 'main',
),

---

// builders_integration/guten_elements/registr/registr.php @ 1.2.0
'pc_align' => array(
	'label'		=> esc_html__('Form alignment', 'privatecontent-free'),
	'type'		=> 'select',
	'opts'		=> array(
		'center'	=> esc_html__('Center', 'privatecontent-free'),
		'left'		=> esc_html__('Left', 'privatecontent-free'),
		'right'		=> esc_html__('Right', 'privatecontent-free'),
	),
	'default' 	=> 'center',
	'panel'		=> 'main',
),

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/privatecontent-free/1.2.0/builders_integration/guten_elements/login/login.php /home/deploy/wp-safety.org/data/plugin-versions/privatecontent-free/1.3.0/builders_integration/guten_elements/login/login.php
--- /home/deploy/wp-safety.org/data/plugin-versions/privatecontent-free/1.2.0/builders_integration/guten_elements/login/login.php	2026-01-20 10:38:14.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/privatecontent-free/1.3.0/builders_integration/guten_elements/login/login.php	2026-04-02 10:00:16.000000000 +0000
@@ -13,12 +13,12 @@
 		'panel'		=> 'main',
 	),
 	'pc_align' => array(
-		'label'		=> esc_html__('Form alignment', 'privatecontent-free'),
+		'label'		=> esc_html__('Form alignment', 'pc_ml'),
 		'type'		=> 'select',
 		'opts'		=> array(
-			'center'	=> esc_html__('Center', 'privatecontent-free'),
-			'left'		=> esc_html__('Left', 'privatecontent-free'),
-			'right'		=> esc_html__('Right', 'privatecontent-free'),
+			'center'	=> esc_html__('Center', 'pc_ml'),
+			'left'		=> esc_html__('Left', 'pc_ml'),
+			'right'		=> esc_html__('Right', 'pc_ml'),
 		),
 		'default' 	=> 'center',
 		'panel'		=> 'main', (truncated)

Exploit Outline

1. Log in to the WordPress site as a user with Contributor-level permissions (or higher). 2. Create a new post or edit an existing one. 3. Insert a shortcode for a PrivateContent form (e.g., [pc-login-form]) and provide a malicious payload for the 'align' attribute, such as: [pc-login-form align=' " onmouseover="alert(document.domain)" ']. 4. Publish the post. 5. View the post as any user (including an Administrator). The double-quote in the payload will close the intended class attribute, allowing the onmouseover event handler to be injected as a new attribute on the form container element. The script executes when a user hovers over the form.

Check if your site is affected.

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